Add cutscene identification and IPC, reorder PathResolver stuff.

This commit is contained in:
Ottermandias 2022-08-09 22:12:15 +02:00
parent dc61f362fd
commit 8fdd173388
21 changed files with 1326 additions and 1323 deletions

@ -1 +1 @@
Subproject commit 4c92479175161617161d8faf844c8f683aa2d5d2 Subproject commit 09dcd012a3106862f20f045b9ff9e33d168047c4

View file

@ -124,6 +124,9 @@ public interface IPenumbraApi : IPenumbraApiBase
// Obtain the game object associated with a given draw object and the name of the collection associated with this game object. // Obtain the game object associated with a given draw object and the name of the collection associated with this game object.
public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ); public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject );
// Obtain the parent game object index for an unnamed cutscene actor by its index.
public int GetCutsceneParentIndex( int actor );
// Obtain a list of all installed mods. The first string is their directory name, the second string is their mod name. // Obtain a list of all installed mods. The first string is their directory name, the second string is their mod name.
public IList< (string, string) > GetModList(); public IList< (string, string) > GetModList();

View file

@ -201,6 +201,7 @@ public class IpcTester : IDisposable
private string _currentDrawObjectString = string.Empty; private string _currentDrawObjectString = string.Empty;
private string _currentReversePath = string.Empty; private string _currentReversePath = string.Empty;
private IntPtr _currentDrawObject = IntPtr.Zero; private IntPtr _currentDrawObject = IntPtr.Zero;
private int _currentCutsceneActor = 0;
private string _lastCreatedGameObjectName = string.Empty; private string _lastCreatedGameObjectName = string.Empty;
private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue; private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue;
@ -231,6 +232,8 @@ public class IpcTester : IDisposable
: IntPtr.Zero; : IntPtr.Zero;
} }
ImGui.InputInt( "Cutscene Actor", ref _currentCutsceneActor, 0 );
using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit ); using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit );
if( !table ) if( !table )
{ {
@ -263,6 +266,10 @@ public class IpcTester : IDisposable
ImGui.TextUnformatted( ptr == IntPtr.Zero ? $"No Actor Associated, {collection}" : $"{ptr:X}, {collection}" ); ImGui.TextUnformatted( ptr == IntPtr.Zero ? $"No Actor Associated, {collection}" : $"{ptr:X}, {collection}" );
} }
DrawIntro( PenumbraIpc.LabelProviderGetDrawObjectInfo, "Cutscene Parent" );
ImGui.TextUnformatted( _pi.GetIpcSubscriber< int, int >( PenumbraIpc.LabelProviderGetCutsceneParentIndex )
.InvokeFunc( _currentCutsceneActor ).ToString() );
DrawIntro( PenumbraIpc.LabelProviderReverseResolvePath, "Reversed Game Paths" ); DrawIntro( PenumbraIpc.LabelProviderReverseResolvePath, "Reversed Game Paths" );
if( _currentReversePath.Length > 0 ) if( _currentReversePath.Length > 0 )
{ {

View file

@ -6,7 +6,6 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging; using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Common.Configuration;
using Lumina.Data; using Lumina.Data;
using Newtonsoft.Json; using Newtonsoft.Json;
using OtterGui; using OtterGui;
@ -16,14 +15,13 @@ using Penumbra.GameData.Enums;
using Penumbra.Interop.Resolver; using Penumbra.Interop.Resolver;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Api; namespace Penumbra.Api;
public class PenumbraApi : IDisposable, IPenumbraApi public class PenumbraApi : IDisposable, IPenumbraApi
{ {
public (int, int) ApiVersion public (int, int) ApiVersion
=> ( 4, 11 ); => ( 4, 12 );
private Penumbra? _penumbra; private Penumbra? _penumbra;
private Lumina.GameData? _lumina; private Lumina.GameData? _lumina;
@ -43,8 +41,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public event CreatingCharacterBaseDelegate? CreatingCharacterBase public event CreatingCharacterBaseDelegate? CreatingCharacterBase
{ {
add => _penumbra!.PathResolver.CreatingCharacterBase += value; add => PathResolver.DrawObjectState.CreatingCharacterBase += value;
remove => _penumbra!.PathResolver.CreatingCharacterBase -= value; remove => PathResolver.DrawObjectState.CreatingCharacterBase -= value;
} }
public bool Valid public bool Valid
@ -54,8 +52,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
_penumbra = penumbra; _penumbra = penumbra;
_lumina = ( Lumina.GameData? )Dalamud.GameData.GetType() _lumina = ( Lumina.GameData? )Dalamud.GameData.GetType()
.GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic ) .GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic )
?.GetValue( Dalamud.GameData ); ?.GetValue( Dalamud.GameData );
foreach( var collection in Penumbra.CollectionManager ) foreach( var collection in Penumbra.CollectionManager )
{ {
SubscribeToCollection( collection ); SubscribeToCollection( collection );
@ -221,10 +219,16 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject ) public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject )
{ {
CheckInitialized(); CheckInitialized();
var (obj, collection) = _penumbra!.PathResolver.IdentifyDrawObject( drawObject ); var (obj, collection) = PathResolver.IdentifyDrawObject( drawObject );
return ( obj, collection.Name ); return ( obj, collection.Name );
} }
public int GetCutsceneParentIndex( int actor )
{
CheckInitialized();
return _penumbra!.PathResolver.CutsceneActor( actor );
}
public IList< (string, string) > GetModList() public IList< (string, string) > GetModList()
{ {
CheckInitialized(); CheckInitialized();
@ -429,7 +433,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
} }
if( !forceOverwriteCharacter && Penumbra.CollectionManager.Characters.ContainsKey( character ) if( !forceOverwriteCharacter && Penumbra.CollectionManager.Characters.ContainsKey( character )
|| Penumbra.TempMods.Collections.ContainsKey( character ) ) || Penumbra.TempMods.Collections.ContainsKey( character ) )
{ {
return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); return ( PenumbraApiEc.CharacterCollectionExists, string.Empty );
} }
@ -475,7 +479,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection )
&& !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) && !Penumbra.CollectionManager.ByName( collectionName, out collection ) )
{ {
return PenumbraApiEc.CollectionMissing; return PenumbraApiEc.CollectionMissing;
} }
@ -512,7 +516,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection )
&& !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) && !Penumbra.CollectionManager.ByName( collectionName, out collection ) )
{ {
return PenumbraApiEc.CollectionMissing; return PenumbraApiEc.CollectionMissing;
} }

View file

@ -261,6 +261,7 @@ public partial class PenumbraIpc
public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath"; public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath";
public const string LabelProviderResolvePlayer = "Penumbra.ResolvePlayerPath"; public const string LabelProviderResolvePlayer = "Penumbra.ResolvePlayerPath";
public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo"; public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo";
public const string LabelProviderGetCutsceneParentIndex = "Penumbra.GetCutsceneParentIndex";
public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath"; public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath";
public const string LabelProviderReverseResolvePlayerPath = "Penumbra.ReverseResolvePlayerPath"; public const string LabelProviderReverseResolvePlayerPath = "Penumbra.ReverseResolvePlayerPath";
public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase"; public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase";
@ -269,6 +270,7 @@ public partial class PenumbraIpc
internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; internal ICallGateProvider< string, string, string >? ProviderResolveCharacter;
internal ICallGateProvider< string, string >? ProviderResolvePlayer; internal ICallGateProvider< string, string >? ProviderResolvePlayer;
internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo; internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo;
internal ICallGateProvider< int, int >? ProviderGetCutsceneParentIndex;
internal ICallGateProvider< string, string, string[] >? ProviderReverseResolvePath; internal ICallGateProvider< string, string, string[] >? ProviderReverseResolvePath;
internal ICallGateProvider< string, string[] >? ProviderReverseResolvePathPlayer; internal ICallGateProvider< string, string[] >? ProviderReverseResolvePathPlayer;
internal ICallGateProvider< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >? ProviderCreatingCharacterBase; internal ICallGateProvider< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >? ProviderCreatingCharacterBase;
@ -315,6 +317,16 @@ public partial class PenumbraIpc
PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" ); PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" );
} }
try
{
ProviderGetCutsceneParentIndex = pi.GetIpcProvider<int, int>( LabelProviderGetCutsceneParentIndex );
ProviderGetCutsceneParentIndex.RegisterFunc( Api.GetCutsceneParentIndex );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetCutsceneParentIndex}:\n{e}" );
}
try try
{ {
ProviderReverseResolvePath = pi.GetIpcProvider< string, string, string[] >( LabelProviderReverseResolvePath ); ProviderReverseResolvePath = pi.GetIpcProvider< string, string, string[] >( LabelProviderReverseResolvePath );
@ -350,6 +362,7 @@ public partial class PenumbraIpc
private void DisposeResolveProviders() private void DisposeResolveProviders()
{ {
ProviderGetDrawObjectInfo?.UnregisterFunc(); ProviderGetDrawObjectInfo?.UnregisterFunc();
ProviderGetCutsceneParentIndex?.UnregisterFunc();
ProviderResolveDefault?.UnregisterFunc(); ProviderResolveDefault?.UnregisterFunc();
ProviderResolveCharacter?.UnregisterFunc(); ProviderResolveCharacter?.UnregisterFunc();
ProviderReverseResolvePath?.UnregisterFunc(); ProviderReverseResolvePath?.UnregisterFunc();

View file

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
namespace Penumbra.Interop.Resolver;
public class CutsceneCharacters : IDisposable
{
public const int CutsceneStartIdx = 200;
public const int CutsceneSlots = 40;
public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots;
private readonly short[] _copiedCharacters = Enumerable.Repeat( ( short )-1, ObjectReloader.CutsceneSlots ).ToArray();
public IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > Actors
=> Enumerable.Range( CutsceneStartIdx, CutsceneSlots )
.Where( i => Dalamud.Objects[ i ] != null )
.Select( i => KeyValuePair.Create( i, this[ i ] ?? Dalamud.Objects[ i ]! ) );
public CutsceneCharacters()
=> SignatureHelper.Initialise( this );
// Get the related actor to a cutscene actor.
// Does not check for valid input index.
// Returns null if no connected actor is set or the actor does not exist anymore.
public global::Dalamud.Game.ClientState.Objects.Types.GameObject? this[ int idx ]
{
get
{
Debug.Assert( idx is >= CutsceneStartIdx and < CutsceneEndIdx );
idx = _copiedCharacters[ idx - CutsceneStartIdx ];
return idx < 0 ? null : Dalamud.Objects[ idx ];
}
}
// Return the currently set index of a parent or -1 if none is set or the index is invalid.
public int GetParentIndex( int idx )
{
if( idx is >= CutsceneStartIdx and < CutsceneEndIdx )
{
return _copiedCharacters[ idx - CutsceneStartIdx ];
}
return -1;
}
public void Enable()
=> _copyCharacterHook.Enable();
public void Disable()
=> _copyCharacterHook.Disable();
public void Dispose()
=> _copyCharacterHook.Dispose();
private unsafe delegate ulong CopyCharacterDelegate( GameObject* target, GameObject* source, uint unk );
[Signature( "E8 ?? ?? ?? ?? 0F B6 9F ?? ?? ?? ?? 48 8D 8F", DetourName = nameof( CopyCharacterDetour ) )]
private readonly Hook< CopyCharacterDelegate > _copyCharacterHook = null!;
private unsafe ulong CopyCharacterDetour( GameObject* target, GameObject* source, uint unk )
{
try
{
if( target != null && target->ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx )
{
var parent = source == null || source->ObjectIndex is < 0 or >= CutsceneStartIdx
? -1
: source->ObjectIndex;
_copiedCharacters[ target->ObjectIndex - CutsceneStartIdx ] = ( short )parent;
}
}
catch
{
// ignored
}
return _copyCharacterHook.Original( target, source, unk );
}
}

View file

@ -1,123 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
private ModCollection? _animationLoadCollection;
private ModCollection? _lastAvfxCollection = null;
public delegate ulong LoadTimelineResourcesDelegate( IntPtr timeline );
// 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.
[Signature( "E8 ?? ?? ?? ?? 83 7F ?? ?? 75 ?? 0F B6 87", DetourName = nameof( LoadTimelineResourcesDetour ) )]
public Hook< LoadTimelineResourcesDelegate >? LoadTimelineResourcesHook;
private ulong LoadTimelineResourcesDetour( IntPtr timeline )
{
ulong ret;
var old = _animationLoadCollection;
try
{
var getGameObjectIdx = ( ( delegate* unmanaged < IntPtr, int>** )timeline )[ 0 ][ 28 ];
var idx = getGameObjectIdx( timeline );
if( idx >= 0 && idx < Dalamud.Objects.Length )
{
var obj = Dalamud.Objects[ idx ];
_animationLoadCollection = obj != null ? IdentifyCollection( ( GameObject* )obj.Address ) : null;
}
else
{
_animationLoadCollection = null;
}
}
finally
{
ret = LoadTimelineResourcesHook!.Original( timeline );
}
_animationLoadCollection = 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.
[Signature( "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05", DetourName = "CharacterBaseLoadAnimationDetour" )]
public Hook< CharacterBaseDestructorDelegate >? CharacterBaseLoadAnimationHook;
private void CharacterBaseLoadAnimationDetour( IntPtr drawObject )
{
var last = _animationLoadCollection;
_animationLoadCollection = _lastCreatedCollection
?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default );
CharacterBaseLoadAnimationHook!.Original( drawObject );
_animationLoadCollection = last;
}
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 ) )]
public Hook< LoadSomeAvfx >? LoadSomeAvfxHook;
private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 )
{
var last = _animationLoadCollection;
_animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject );
var ret = LoadSomeAvfxHook!.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 );
_animationLoadCollection = last;
return ret;
}
// Unknown what exactly this is but it seems to load a bunch of paps.
public delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 );
[Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51" )]
public Hook< LoadSomePap >? LoadSomePapHook;
private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 )
{
var timelinePtr = a1 + 0x50;
var last = _animationLoadCollection;
if( timelinePtr != IntPtr.Zero )
{
var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 );
if( actorIdx >= 0 && actorIdx < Dalamud.Objects.Length )
{
_animationLoadCollection = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) );
}
}
LoadSomePapHook!.Original( a1, a2, a3, a4 );
_animationLoadCollection = last;
}
// Seems to load character actions when zoning or changing class, maybe.
[Signature( "E8 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ?? 8B 8E", DetourName = nameof( SomeActionLoadDetour ) )]
public Hook< CharacterBaseDestructorDelegate >? SomeActionLoadHook;
private void SomeActionLoadDetour( IntPtr gameObject )
{
var last = _animationLoadCollection;
_animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject );
SomeActionLoadHook!.Original( gameObject );
_animationLoadCollection = last;
}
[Signature( "E8 ?? ?? ?? ?? 44 84 BB", DetourName = nameof( SomeOtherAvfxDetour ) )]
public Hook< CharacterBaseDestructorDelegate >? SomeOtherAvfxHook;
private void SomeOtherAvfxDetour( IntPtr unk )
{
var last = _animationLoadCollection;
var gameObject = ( GameObject* )( unk - 0x8B0 );
_animationLoadCollection = IdentifyCollection( gameObject );
SomeOtherAvfxHook!.Original( unk );
_animationLoadCollection = last;
}
}

View file

@ -0,0 +1,215 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
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 ModCollection? _animationLoadCollection;
private ModCollection? _lastAvfxCollection;
public AnimationState( DrawObjectState drawObjectState )
{
_drawObjectState = drawObjectState;
SignatureHelper.Initialise( this );
}
public bool HandleFiles( ResourceType type, Utf8GamePath _, [NotNullWhen( true )] out ModCollection? collection )
{
switch( type )
{
case ResourceType.Tmb:
case ResourceType.Pap:
case ResourceType.Scd:
if( _animationLoadCollection != null )
{
collection = _animationLoadCollection;
return true;
}
break;
case ResourceType.Avfx:
_lastAvfxCollection = _animationLoadCollection ?? Penumbra.CollectionManager.Default;
if( _animationLoadCollection != null )
{
collection = _animationLoadCollection;
return true;
}
break;
case ResourceType.Atex:
if( _lastAvfxCollection != null )
{
collection = _lastAvfxCollection;
return true;
}
if( _animationLoadCollection != null )
{
collection = _animationLoadCollection;
return true;
}
break;
}
collection = null;
return false;
}
public void Enable()
{
_loadTimelineResourcesHook.Enable();
_characterBaseLoadAnimationHook.Enable();
_loadSomeAvfxHook.Enable();
_loadSomePapHook.Enable();
_someActionLoadHook.Enable();
_someOtherAvfxHook.Enable();
}
public void Disable()
{
_loadTimelineResourcesHook.Disable();
_characterBaseLoadAnimationHook.Disable();
_loadSomeAvfxHook.Disable();
_loadSomePapHook.Disable();
_someActionLoadHook.Disable();
_someOtherAvfxHook.Disable();
}
public void Dispose()
{
_loadTimelineResourcesHook.Dispose();
_characterBaseLoadAnimationHook.Dispose();
_loadSomeAvfxHook.Dispose();
_loadSomePapHook.Dispose();
_someActionLoadHook.Dispose();
_someOtherAvfxHook.Dispose();
}
// 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( "E8 ?? ?? ?? ?? 83 7F ?? ?? 75 ?? 0F B6 87", DetourName = nameof( LoadTimelineResourcesDetour ) )]
private readonly Hook< LoadTimelineResourcesDelegate > _loadTimelineResourcesHook = null!;
private ulong LoadTimelineResourcesDetour( IntPtr timeline )
{
ulong ret;
var old = _animationLoadCollection;
try
{
var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ 28 ];
var idx = getGameObjectIdx( timeline );
if( idx >= 0 && idx < Dalamud.Objects.Length )
{
var obj = Dalamud.Objects[ idx ];
_animationLoadCollection = obj != null ? IdentifyCollection( ( GameObject* )obj.Address ) : null;
}
else
{
_animationLoadCollection = null;
}
}
finally
{
ret = _loadTimelineResourcesHook!.Original( timeline );
}
_animationLoadCollection = 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( "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05",
DetourName = nameof( CharacterBaseLoadAnimationDetour ) )]
private readonly Hook< CharacterBaseNoArgumentDelegate > _characterBaseLoadAnimationHook = null!;
private void CharacterBaseLoadAnimationDetour( IntPtr drawObject )
{
var last = _animationLoadCollection;
_animationLoadCollection = _drawObjectState.LastCreatedCollection
?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default );
_characterBaseLoadAnimationHook!.Original( drawObject );
_animationLoadCollection = last;
}
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 = _animationLoadCollection;
_animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject );
var ret = _loadSomeAvfxHook!.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 );
_animationLoadCollection = last;
return ret;
}
// 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( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51",
DetourName = nameof( LoadSomePapDetour ) )]
private readonly Hook< LoadSomePap > _loadSomePapHook = null!;
private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 )
{
var timelinePtr = a1 + 0x50;
var last = _animationLoadCollection;
if( timelinePtr != IntPtr.Zero )
{
var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 );
if( actorIdx >= 0 && actorIdx < Dalamud.Objects.Length )
{
_animationLoadCollection = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) );
}
}
_loadSomePapHook!.Original( a1, a2, a3, a4 );
_animationLoadCollection = last;
}
// Seems to load character actions when zoning or changing class, maybe.
[Signature( "E8 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ?? 8B 8E", DetourName = nameof( SomeActionLoadDetour ) )]
private readonly Hook< CharacterBaseNoArgumentDelegate > _someActionLoadHook = null!;
private void SomeActionLoadDetour( IntPtr gameObject )
{
var last = _animationLoadCollection;
_animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject );
_someActionLoadHook!.Original( gameObject );
_animationLoadCollection = last;
}
[Signature( "E8 ?? ?? ?? ?? 44 84 BB", DetourName = nameof( SomeOtherAvfxDetour ) )]
private readonly Hook< CharacterBaseNoArgumentDelegate > _someOtherAvfxHook = null!;
private void SomeOtherAvfxDetour( IntPtr unk )
{
var last = _animationLoadCollection;
var gameObject = ( GameObject* )( unk - 0x8B0 );
_animationLoadCollection = IdentifyCollection( gameObject );
_someOtherAvfxHook!.Original( unk );
_animationLoadCollection = last;
}
}
}

View file

@ -1,88 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
[Signature( "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA", ScanType = ScanType.StaticAddress )]
public IntPtr* DrawObjectDemiVTable;
public Hook< GeneralResolveDelegate >? ResolveDemiDecalPathHook;
public Hook< EidResolveDelegate >? ResolveDemiEidPathHook;
public Hook< GeneralResolveDelegate >? ResolveDemiImcPathHook;
public Hook< MPapResolveDelegate >? ResolveDemiMPapPathHook;
public Hook< GeneralResolveDelegate >? ResolveDemiMdlPathHook;
public Hook< MaterialResolveDetour >? ResolveDemiMtrlPathHook;
public Hook< MaterialResolveDetour >? ResolveDemiPapPathHook;
public Hook< GeneralResolveDelegate >? ResolveDemiPhybPathHook;
public Hook< GeneralResolveDelegate >? ResolveDemiSklbPathHook;
public Hook< GeneralResolveDelegate >? ResolveDemiSkpPathHook;
public Hook< EidResolveDelegate >? ResolveDemiTmbPathHook;
public Hook< MaterialResolveDetour >? ResolveDemiVfxPathHook;
private void SetupDemiHooks()
{
ResolveDemiDecalPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveDecalIdx ], ResolveDemiDecalDetour );
ResolveDemiEidPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveEidIdx ], ResolveDemiEidDetour );
ResolveDemiImcPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveImcIdx ], ResolveDemiImcDetour );
ResolveDemiMPapPathHook = Hook< MPapResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveMPapIdx ], ResolveDemiMPapDetour );
ResolveDemiMdlPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveMdlIdx ], ResolveDemiMdlDetour );
ResolveDemiMtrlPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectDemiVTable[ ResolveMtrlIdx ], ResolveDemiMtrlDetour );
ResolveDemiPapPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectDemiVTable[ ResolvePapIdx ], ResolveDemiPapDetour );
ResolveDemiPhybPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolvePhybIdx ], ResolveDemiPhybDetour );
ResolveDemiSklbPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveSklbIdx ], ResolveDemiSklbDetour );
ResolveDemiSkpPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveSkpIdx ], ResolveDemiSkpDetour );
ResolveDemiTmbPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveTmbIdx ], ResolveDemiTmbDetour );
ResolveDemiVfxPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectDemiVTable[ ResolveVfxIdx ], ResolveDemiVfxDetour );
}
private void EnableDemiHooks()
{
ResolveDemiDecalPathHook?.Enable();
ResolveDemiEidPathHook?.Enable();
ResolveDemiImcPathHook?.Enable();
ResolveDemiMPapPathHook?.Enable();
ResolveDemiMdlPathHook?.Enable();
ResolveDemiMtrlPathHook?.Enable();
ResolveDemiPapPathHook?.Enable();
ResolveDemiPhybPathHook?.Enable();
ResolveDemiSklbPathHook?.Enable();
ResolveDemiSkpPathHook?.Enable();
ResolveDemiTmbPathHook?.Enable();
ResolveDemiVfxPathHook?.Enable();
}
private void DisableDemiHooks()
{
ResolveDemiDecalPathHook?.Disable();
ResolveDemiEidPathHook?.Disable();
ResolveDemiImcPathHook?.Disable();
ResolveDemiMPapPathHook?.Disable();
ResolveDemiMdlPathHook?.Disable();
ResolveDemiMtrlPathHook?.Disable();
ResolveDemiPapPathHook?.Disable();
ResolveDemiPhybPathHook?.Disable();
ResolveDemiSklbPathHook?.Disable();
ResolveDemiSkpPathHook?.Disable();
ResolveDemiTmbPathHook?.Disable();
ResolveDemiVfxPathHook?.Disable();
}
private void DisposeDemiHooks()
{
ResolveDemiDecalPathHook?.Dispose();
ResolveDemiEidPathHook?.Dispose();
ResolveDemiImcPathHook?.Dispose();
ResolveDemiMPapPathHook?.Dispose();
ResolveDemiMdlPathHook?.Dispose();
ResolveDemiMtrlPathHook?.Dispose();
ResolveDemiPapPathHook?.Dispose();
ResolveDemiPhybPathHook?.Dispose();
ResolveDemiSklbPathHook?.Dispose();
ResolveDemiSkpPathHook?.Dispose();
ResolveDemiTmbPathHook?.Dispose();
ResolveDemiVfxPathHook?.Dispose();
}
}

View file

@ -0,0 +1,234 @@
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.Api;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
public class DrawObjectState
{
public static event CreatingCharacterBaseDelegate? CreatingCharacterBase;
public IEnumerable< KeyValuePair< IntPtr, (ModCollection, int) > > DrawObjects
=> _drawObjectToObject;
public int Count
=> _drawObjectToObject.Count;
public bool TryGetValue( IntPtr drawObject, out (ModCollection, 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 ModCollection? CheckParentDrawObject( IntPtr drawObject, IntPtr parentObject )
{
if( parentObject == IntPtr.Zero && LastGameObject != null )
{
var collection = IdentifyCollection( LastGameObject );
_drawObjectToObject[ drawObject ] = ( collection, LastGameObject->ObjectIndex );
return collection;
}
return null;
}
public bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, [NotNullWhen( true )] out ModCollection? collection )
{
if( type == ResourceType.Tex
&& LastCreatedCollection != null
&& gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l', '_', 'f', 'a', 'c', 'e' ) )
{
collection = LastCreatedCollection!;
return true;
}
collection = null;
return false;
}
public ModCollection? LastCreatedCollection
=> _lastCreatedCollection;
public GameObject* LastGameObject { get; private set; }
public DrawObjectState()
{
SignatureHelper.Initialise( this );
}
public void Enable()
{
_characterBaseCreateHook.Enable();
_characterBaseDestructorHook.Enable();
_enableDrawHook.Enable();
_weaponReloadHook.Enable();
InitializeDrawObjects();
Penumbra.CollectionManager.CollectionChanged += CheckCollections;
}
public void Disable()
{
_characterBaseCreateHook.Disable();
_characterBaseDestructorHook.Disable();
_enableDrawHook.Disable();
_weaponReloadHook.Disable();
Penumbra.CollectionManager.CollectionChanged -= 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* )Dalamud.Objects.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, (ModCollection, int) > _drawObjectToObject = new();
private ModCollection? _lastCreatedCollection;
// 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( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40", DetourName = nameof( CharacterBaseCreateDetour ) )]
private readonly Hook< CharacterBaseCreateDelegate > _characterBaseCreateHook = null!;
private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d )
{
using var cmp = MetaChanger.ChangeCmp( LastGameObject, out _lastCreatedCollection );
if( LastGameObject != null )
{
var modelPtr = &a;
CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, ( IntPtr )modelPtr, b, c );
}
var ret = _characterBaseCreateHook!.Original( a, b, c, d );
if( LastGameObject != null )
{
_drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex );
}
return ret;
}
// Remove DrawObjects from the list when they are destroyed.
private delegate void CharacterBaseDestructorDelegate( IntPtr drawBase );
[Signature( "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30",
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( "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? 33 D2 E8 ?? ?? ?? ?? 84 C0", 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( "E8 ?? ?? ?? ?? 44 8B 9F", 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? name )
{
if( type is CollectionType.Inactive or CollectionType.Current )
{
return;
}
foreach( var (key, (_, idx)) in _drawObjectToObject.ToArray() )
{
if( !VerifyEntry( key, idx, out var obj ) )
{
_drawObjectToObject.Remove( key );
}
var newCollection = IdentifyCollection( obj );
_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 < Dalamud.Objects.Length; ++i )
{
var ptr = ( GameObject* )Dalamud.Objects.GetObjectAddress( i );
if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null )
{
_drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr ), ptr->ObjectIndex );
}
}
}
}
}

View file

@ -1,118 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
namespace Penumbra.Interop.Resolver;
// We can hook the different Resolve-Functions using just the VTable of Human.
// The other DrawObject VTables and the ResolveRoot function are currently unused.
public unsafe partial class PathResolver
{
[Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1", ScanType = ScanType.StaticAddress )]
public IntPtr* DrawObjectHumanVTable;
// [Signature( "48 8D 1D ?? ?? ?? ?? 48 C7 41", ScanType = ScanType.StaticAddress )]
// public IntPtr* DrawObjectVTable;
//
// public const int ResolveRootIdx = 71;
public const int ResolveSklbIdx = 72;
public const int ResolveMdlIdx = 73;
public const int ResolveSkpIdx = 74;
public const int ResolvePhybIdx = 75;
public const int ResolvePapIdx = 76;
public const int ResolveTmbIdx = 77;
public const int ResolveMPapIdx = 79;
public const int ResolveImcIdx = 81;
public const int ResolveMtrlIdx = 82;
public const int ResolveDecalIdx = 83;
public const int ResolveVfxIdx = 84;
public const int ResolveEidIdx = 85;
public const int OnModelLoadCompleteIdx = 58;
public delegate IntPtr GeneralResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 );
public delegate IntPtr MPapResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 );
public delegate IntPtr MaterialResolveDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 );
public delegate IntPtr EidResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3 );
public delegate void OnModelLoadCompleteDelegate( IntPtr drawObject );
public Hook< GeneralResolveDelegate >? ResolveDecalPathHook;
public Hook< EidResolveDelegate >? ResolveEidPathHook;
public Hook< GeneralResolveDelegate >? ResolveImcPathHook;
public Hook< MPapResolveDelegate >? ResolveMPapPathHook;
public Hook< GeneralResolveDelegate >? ResolveMdlPathHook;
public Hook< MaterialResolveDetour >? ResolveMtrlPathHook;
public Hook< MaterialResolveDetour >? ResolvePapPathHook;
public Hook< GeneralResolveDelegate >? ResolvePhybPathHook;
public Hook< GeneralResolveDelegate >? ResolveSklbPathHook;
public Hook< GeneralResolveDelegate >? ResolveSkpPathHook;
public Hook< EidResolveDelegate >? ResolveTmbPathHook;
public Hook< MaterialResolveDetour >? ResolveVfxPathHook;
private void SetupHumanHooks()
{
ResolveDecalPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveDecalIdx ], ResolveDecalDetour );
ResolveEidPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveEidIdx ], ResolveEidDetour );
ResolveImcPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveImcIdx ], ResolveImcDetour );
ResolveMPapPathHook = Hook< MPapResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveMPapIdx ], ResolveMPapDetour );
ResolveMdlPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveMdlIdx ], ResolveMdlDetour );
ResolveMtrlPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectHumanVTable[ ResolveMtrlIdx ], ResolveMtrlDetour );
ResolvePapPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectHumanVTable[ ResolvePapIdx ], ResolvePapDetour );
ResolvePhybPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolvePhybIdx ], ResolvePhybDetour );
ResolveSklbPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveSklbIdx ], ResolveSklbDetour );
ResolveSkpPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveSkpIdx ], ResolveSkpDetour );
ResolveTmbPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveTmbIdx ], ResolveTmbDetour );
ResolveVfxPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectHumanVTable[ ResolveVfxIdx ], ResolveVfxDetour );
}
private void EnableHumanHooks()
{
ResolveDecalPathHook?.Enable();
ResolveEidPathHook?.Enable();
ResolveImcPathHook?.Enable();
ResolveMPapPathHook?.Enable();
ResolveMdlPathHook?.Enable();
ResolveMtrlPathHook?.Enable();
ResolvePapPathHook?.Enable();
ResolvePhybPathHook?.Enable();
ResolveSklbPathHook?.Enable();
ResolveSkpPathHook?.Enable();
ResolveTmbPathHook?.Enable();
ResolveVfxPathHook?.Enable();
}
private void DisableHumanHooks()
{
ResolveDecalPathHook?.Disable();
ResolveEidPathHook?.Disable();
ResolveImcPathHook?.Disable();
ResolveMPapPathHook?.Disable();
ResolveMdlPathHook?.Disable();
ResolveMtrlPathHook?.Disable();
ResolvePapPathHook?.Disable();
ResolvePhybPathHook?.Disable();
ResolveSklbPathHook?.Disable();
ResolveSkpPathHook?.Disable();
ResolveTmbPathHook?.Disable();
ResolveVfxPathHook?.Disable();
}
private void DisposeHumanHooks()
{
ResolveDecalPathHook?.Dispose();
ResolveEidPathHook?.Dispose();
ResolveImcPathHook?.Dispose();
ResolveMPapPathHook?.Dispose();
ResolveMdlPathHook?.Dispose();
ResolveMtrlPathHook?.Dispose();
ResolvePapPathHook?.Dispose();
ResolvePhybPathHook?.Dispose();
ResolveSklbPathHook?.Dispose();
ResolveSkpPathHook?.Dispose();
ResolveTmbPathHook?.Dispose();
ResolveVfxPathHook?.Dispose();
}
}

View file

@ -1,17 +1,10 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Logging; using Dalamud.Logging;
using Dalamud.Utility.Signatures;
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 FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using Penumbra.Api;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -22,148 +15,6 @@ namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver public unsafe partial class PathResolver
{ {
// Keep track of created DrawObjects that are CharacterBase,
// and use the last game object that called EnableDraw to link them.
public delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d );
[Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40", DetourName = "CharacterBaseCreateDetour" )]
public Hook< CharacterBaseCreateDelegate >? CharacterBaseCreateHook;
private ModCollection? _lastCreatedCollection;
public event CreatingCharacterBaseDelegate? CreatingCharacterBase;
private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d )
{
using var cmp = MetaChanger.ChangeCmp( this, out _lastCreatedCollection );
if( LastGameObject != null )
{
var modelPtr = &a;
CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, ( IntPtr )modelPtr, b, c );
}
var ret = CharacterBaseCreateHook!.Original( a, b, c, d );
if( LastGameObject != null )
{
DrawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex );
}
return ret;
}
// Remove DrawObjects from the list when they are destroyed.
public delegate void CharacterBaseDestructorDelegate( IntPtr drawBase );
[Signature( "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30",
DetourName = "CharacterBaseDestructorDetour" )]
public Hook< CharacterBaseDestructorDelegate >? CharacterBaseDestructorHook;
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.
public delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d );
[Signature( "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? 33 D2 E8 ?? ?? ?? ?? 84 C0" )]
public Hook< EnableDrawDelegate >? EnableDrawHook;
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.
public delegate void WeaponReloadFunc( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 );
[Signature( "E8 ?? ?? ?? ?? 44 8B 9F" )]
public Hook< WeaponReloadFunc >? WeaponReloadHook;
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;
}
private void EnableDataHooks()
{
CharacterBaseCreateHook?.Enable();
EnableDrawHook?.Enable();
CharacterBaseDestructorHook?.Enable();
WeaponReloadHook?.Enable();
Penumbra.CollectionManager.CollectionChanged += CheckCollections;
LoadTimelineResourcesHook?.Enable();
CharacterBaseLoadAnimationHook?.Enable();
LoadSomeAvfxHook?.Enable();
LoadSomePapHook?.Enable();
SomeActionLoadHook?.Enable();
SomeOtherAvfxHook?.Enable();
}
private void DisableDataHooks()
{
Penumbra.CollectionManager.CollectionChanged -= CheckCollections;
WeaponReloadHook?.Disable();
CharacterBaseCreateHook?.Disable();
EnableDrawHook?.Disable();
CharacterBaseDestructorHook?.Disable();
LoadTimelineResourcesHook?.Disable();
CharacterBaseLoadAnimationHook?.Disable();
LoadSomeAvfxHook?.Disable();
LoadSomePapHook?.Disable();
SomeActionLoadHook?.Disable();
SomeOtherAvfxHook?.Disable();
}
private void DisposeDataHooks()
{
WeaponReloadHook?.Dispose();
CharacterBaseCreateHook?.Dispose();
EnableDrawHook?.Dispose();
CharacterBaseDestructorHook?.Dispose();
LoadTimelineResourcesHook?.Dispose();
CharacterBaseLoadAnimationHook?.Dispose();
LoadSomeAvfxHook?.Dispose();
LoadSomePapHook?.Dispose();
SomeActionLoadHook?.Dispose();
SomeOtherAvfxHook?.Dispose();
}
// 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.
internal readonly Dictionary< IntPtr, (ModCollection, int) > DrawObjectToObject = new();
// This map links files to their corresponding collection, if it is non-default.
internal readonly ConcurrentDictionary< Utf8String, ModCollection > PathCollections = new();
internal GameObject* LastGameObject = null;
// 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* )Dalamud.Objects.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;
}
// Obtain the name of the current player, if one exists. // Obtain the name of the current player, if one exists.
private static string? GetPlayerName() private static string? GetPlayerName()
=> Dalamud.Objects[ 0 ]?.Name.ToString(); => Dalamud.Objects[ 0 ]?.Name.ToString();
@ -244,6 +95,13 @@ public unsafe partial class PathResolver
return null; return null;
} }
var parent = Cutscenes[ gameObject->ObjectIndex ];
if( parent != null )
{
return parent.Name.ToString();
}
// should not really happen but keep it in as a emergency case.
var player = Dalamud.Objects[ 0 ]; var player = Dalamud.Objects[ 0 ];
if( player == null ) if( player == null )
{ {
@ -327,12 +185,14 @@ public unsafe partial class PathResolver
// Only OwnerName can be applied to something with a non-empty name, and that is the specific case we want to handle. // Only OwnerName can be applied to something with a non-empty name, and that is the specific case we want to handle.
var actualName = gameObject->ObjectIndex switch var actualName = gameObject->ObjectIndex switch
{ {
240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window 240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window
241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor.
242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on 242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on
243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview 243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview
>= 200 => GetCutsceneName( gameObject ),
_ => null, >= ObjectReloader.CutsceneStartIdx and < ObjectReloader.CutsceneEndIdx => GetCutsceneName( gameObject ),
_ => null,
} }
?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString();
@ -466,77 +326,4 @@ public unsafe partial class PathResolver
return false; return false;
} }
// Update collections linked to Game/DrawObjects due to a change in collection configuration.
private void CheckCollections( CollectionType type, ModCollection? _1, ModCollection? _2, string? name )
{
if( type is CollectionType.Inactive or CollectionType.Current )
{
return;
}
foreach( var (key, (_, idx)) in DrawObjectToObject.ToArray() )
{
if( !VerifyEntry( key, idx, out var obj ) )
{
DrawObjectToObject.Remove( key );
}
var newCollection = IdentifyCollection( obj );
DrawObjectToObject[ key ] = ( newCollection, idx );
}
}
// Use the stored information to find the GameObject and Collection linked to a DrawObject.
private GameObject* FindParent( IntPtr drawObject, out ModCollection collection )
{
if( DrawObjectToObject.TryGetValue( drawObject, out var data ) )
{
var gameObjectIdx = data.Item2;
if( VerifyEntry( drawObject, gameObjectIdx, out var gameObject ) )
{
collection = data.Item1;
return gameObject;
}
}
if( LastGameObject != null && ( LastGameObject->DrawObject == null || LastGameObject->DrawObject == ( DrawObject* )drawObject ) )
{
collection = IdentifyCollection( LastGameObject );
return LastGameObject;
}
collection = IdentifyCollection( null );
return null;
}
// Special handling for paths so that we do not store non-owned temporary strings in the dictionary.
private void SetCollection( Utf8String path, ModCollection collection )
{
if( PathCollections.ContainsKey( path ) || path.IsOwned )
{
PathCollections[ path ] = collection;
}
else
{
PathCollections[ path.Clone() ] = collection;
}
}
// 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 < Dalamud.Objects.Length; ++i )
{
var ptr = ( GameObject* )Dalamud.Objects.GetObjectAddress( i );
if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null )
{
DrawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr ), ptr->ObjectIndex );
}
}
}
} }

View file

@ -4,7 +4,6 @@ using Dalamud.Hooking;
using Dalamud.Logging; using Dalamud.Logging;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -12,132 +11,144 @@ using Penumbra.Interop.Structs;
namespace Penumbra.Interop.Resolver; namespace Penumbra.Interop.Resolver;
// Materials do contain their own paths to textures and shader packages.
// Those are loaded synchronously.
// Thus, we need to ensure the correct files are loaded when a material is loaded.
public unsafe partial class PathResolver public unsafe partial class PathResolver
{ {
public delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle ); // Materials do contain their own paths to textures and shader packages.
// Those are loaded synchronously.
[Signature( "4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = "LoadMtrlTexDetour" )] // Thus, we need to ensure the correct files are loaded when a material is loaded.
public Hook< LoadMtrlFilesDelegate >? LoadMtrlTexHook; public class MaterialState : IDisposable
private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle )
{ {
LoadMtrlHelper( mtrlResourceHandle ); private readonly PathState _paths;
var ret = LoadMtrlTexHook!.Original( mtrlResourceHandle );
_mtrlCollection = null;
return ret;
}
[Signature( "48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B7 89", private ModCollection? _mtrlCollection;
DetourName = "LoadMtrlShpkDetour" )]
public Hook< LoadMtrlFilesDelegate >? LoadMtrlShpkHook;
private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle ) public MaterialState( PathState paths )
{
LoadMtrlHelper( mtrlResourceHandle );
var ret = LoadMtrlShpkHook!.Original( mtrlResourceHandle );
_mtrlCollection = null;
return ret;
}
private ModCollection? _mtrlCollection;
private void LoadMtrlHelper( IntPtr mtrlResourceHandle )
{
if( mtrlResourceHandle == IntPtr.Zero )
{ {
return; SignatureHelper.Initialise( this );
_paths = paths;
} }
var mtrl = ( MtrlResource* )mtrlResourceHandle; // Check specifically for shpk and tex files whether we are currently in a material load.
var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true ); public bool HandleSubFiles( ResourceType type, [NotNullWhen( true )] out ModCollection? collection )
_mtrlCollection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null;
}
// Check specifically for shpk and tex files whether we are currently in a material load.
private bool HandleMaterialSubFiles( ResourceType type, [NotNullWhen( true )] out ModCollection? collection )
{
if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk )
{ {
collection = _mtrlCollection; if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk )
return true; {
} collection = _mtrlCollection;
return true;
}
collection = null; collection = null;
return false;
}
// We need to set the correct collection for the actual material path that is loaded
// before actually loading the file.
private bool MtrlLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager,
SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret )
{
ret = 0;
if( fileDescriptor->ResourceHandle->FileType != ResourceType.Mtrl )
{
return false; return false;
} }
var lastUnderscore = split.LastIndexOf( ( byte )'_' ); // Materials need to be set per collection so they can load their textures independently from each other.
var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); public static void HandleCollection( ModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved,
if( Penumbra.TempMods.CollectionByName( name, out var collection ) out (FullPath?, object?) data )
|| Penumbra.CollectionManager.ByName( name, out collection ) )
{ {
if( nonDefault && type == ResourceType.Mtrl )
{
var fullPath = new FullPath( $"|{collection.Name}_{collection.ChangeCounter}|{path}" );
data = ( fullPath, collection );
}
else
{
data = ( resolved, collection );
}
}
public void Enable()
{
_loadMtrlShpkHook.Enable();
_loadMtrlTexHook.Enable();
Penumbra.ResourceLoader.ResourceLoadCustomization += MtrlLoadHandler;
}
public void Disable()
{
_loadMtrlShpkHook.Disable();
_loadMtrlTexHook.Disable();
Penumbra.ResourceLoader.ResourceLoadCustomization -= MtrlLoadHandler;
}
public void Dispose()
{
Disable();
_loadMtrlShpkHook?.Dispose();
_loadMtrlTexHook?.Dispose();
}
// We need to set the correct collection for the actual material path that is loaded
// before actually loading the file.
public bool MtrlLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager,
SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret )
{
ret = 0;
if( fileDescriptor->ResourceHandle->FileType != ResourceType.Mtrl )
{
return false;
}
var lastUnderscore = split.LastIndexOf( ( byte )'_' );
var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString();
if( Penumbra.TempMods.CollectionByName( name, out var collection )
|| Penumbra.CollectionManager.ByName( name, out collection ) )
{
#if DEBUG #if DEBUG
PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path ); PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path );
#endif #endif
SetCollection( path, collection ); _paths.SetCollection( path, collection );
} }
else else
{ {
#if DEBUG #if DEBUG
PluginLog.Verbose( "Using MtrlLoadHandler with no collection for path {$Path:l}.", path ); PluginLog.Verbose( "Using MtrlLoadHandler with no collection for path {$Path:l}.", path );
#endif #endif
}
// Force isSync = true for this call. I don't really understand why,
// or where the difference even comes from.
// Was called with True on my client and with false on other peoples clients,
// which caused problems.
ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, true );
_paths.Consume( path, out _ );
return true;
} }
// Force isSync = true for this call. I don't really understand why, private delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle );
// or where the difference even comes from.
// Was called with True on my client and with false on other peoples clients,
// which caused problems.
ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, true );
PathCollections.TryRemove( path, out _ );
return true;
}
// Materials need to be set per collection so they can load their textures independently from each other. [Signature( "4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = nameof( LoadMtrlTexDetour ) )]
private static void HandleMtrlCollection( ModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, private readonly Hook< LoadMtrlFilesDelegate > _loadMtrlTexHook = null!;
out (FullPath?, object?) data )
{ private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle )
if( nonDefault && type == ResourceType.Mtrl )
{ {
var fullPath = new FullPath( $"|{collection.Name}_{collection.ChangeCounter}|{path}" ); LoadMtrlHelper( mtrlResourceHandle );
data = ( fullPath, collection ); var ret = _loadMtrlTexHook!.Original( mtrlResourceHandle );
_mtrlCollection = null;
return ret;
} }
else
[Signature( "48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B7 89",
DetourName = nameof( LoadMtrlShpkDetour ) )]
private readonly Hook< LoadMtrlFilesDelegate > _loadMtrlShpkHook = null!;
private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle )
{ {
data = ( resolved, collection ); LoadMtrlHelper( mtrlResourceHandle );
var ret = _loadMtrlShpkHook!.Original( mtrlResourceHandle );
_mtrlCollection = null;
return ret;
} }
}
private void EnableMtrlHooks() private void LoadMtrlHelper( IntPtr mtrlResourceHandle )
{ {
LoadMtrlShpkHook?.Enable(); if( mtrlResourceHandle == IntPtr.Zero )
LoadMtrlTexHook?.Enable(); {
Penumbra.ResourceLoader.ResourceLoadCustomization += MtrlLoadHandler; return;
} }
private void DisableMtrlHooks() var mtrl = ( MtrlResource* )mtrlResourceHandle;
{ var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true );
LoadMtrlShpkHook?.Disable(); _mtrlCollection = _paths.TryGetValue( mtrlPath, out var c ) ? c : null;
LoadMtrlTexHook?.Disable(); }
Penumbra.ResourceLoader.ResourceLoadCustomization -= MtrlLoadHandler;
}
private void DisposeMtrlHooks()
{
LoadMtrlShpkHook?.Dispose();
LoadMtrlTexHook?.Dispose();
} }
} }

View file

@ -1,9 +1,8 @@
using System; using System;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
namespace Penumbra.Interop.Resolver; namespace Penumbra.Interop.Resolver;
@ -33,136 +32,133 @@ namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver public unsafe partial class PathResolver
{ {
public delegate void UpdateModelDelegate( IntPtr drawObject ); public unsafe class MetaState : IDisposable
[Signature( "48 8B ?? 56 48 83 ?? ?? ?? B9", DetourName = "UpdateModelsDetour" )]
public Hook< UpdateModelDelegate >? UpdateModelsHook;
private void UpdateModelsDetour( IntPtr drawObject )
{ {
// Shortcut because this is called all the time. private readonly PathResolver _parent;
// Same thing is checked at the beginning of the original function.
if( *( int* )( drawObject + 0x90c ) == 0 ) public MetaState( PathResolver parent, IntPtr* humanVTable )
{ {
return; SignatureHelper.Initialise( this );
_parent = parent;
_onModelLoadCompleteHook = Hook< OnModelLoadCompleteDelegate >.FromAddress( humanVTable[ 58 ], OnModelLoadCompleteDetour );
} }
var collection = GetCollection( drawObject ); public void Enable()
if( collection != null )
{ {
using var eqp = MetaChanger.ChangeEqp( collection ); _getEqpIndirectHook.Enable();
using var eqdp = MetaChanger.ChangeEqdp( collection ); _updateModelsHook.Enable();
UpdateModelsHook!.Original.Invoke( drawObject ); _onModelLoadCompleteHook.Enable();
} _setupVisorHook.Enable();
else _rspSetupCharacterHook.Enable();
{
UpdateModelsHook!.Original.Invoke( drawObject );
}
}
[Signature( "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C",
DetourName = "GetEqpIndirectDetour" )]
public Hook< OnModelLoadCompleteDelegate >? GetEqpIndirectHook;
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 + 0xa30 ) & 1 ) == 0 || *( ulong* )( drawObject + 0xa28 ) == 0 )
{
return;
} }
using var eqp = MetaChanger.ChangeEqp( this, drawObject ); public void Disable()
GetEqpIndirectHook!.Original( drawObject );
}
public Hook< OnModelLoadCompleteDelegate >? OnModelLoadCompleteHook;
private void OnModelLoadCompleteDetour( IntPtr drawObject )
{
var collection = GetCollection( drawObject );
if( collection != null )
{ {
using var eqp = MetaChanger.ChangeEqp( collection ); _getEqpIndirectHook.Disable();
using var eqdp = MetaChanger.ChangeEqdp( collection ); _updateModelsHook.Disable();
OnModelLoadCompleteHook!.Original.Invoke( drawObject ); _onModelLoadCompleteHook.Disable();
} _setupVisorHook.Disable();
else _rspSetupCharacterHook.Disable();
{
OnModelLoadCompleteHook!.Original.Invoke( 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.
public delegate byte SetupVisorDelegate( IntPtr drawObject, ushort modelId, byte visorState );
[Signature( "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", DetourName = "SetupVisorDetour" )]
public Hook< SetupVisorDelegate >? SetupVisorHook;
private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState )
{
using var gmp = MetaChanger.ChangeGmp( this, drawObject );
return SetupVisorHook!.Original( drawObject, modelId, visorState );
}
// RSP
public delegate void RspSetupCharacterDelegate( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 );
[Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 88 54 24 ?? 57 41 56", DetourName = "RspSetupCharacterDetour" )]
public Hook< RspSetupCharacterDelegate >? RspSetupCharacterHook;
private void RspSetupCharacterDetour( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 )
{
using var rsp = MetaChanger.ChangeCmp( this, drawObject );
RspSetupCharacterHook!.Original( drawObject, unk2, unk3, unk4, unk5 );
}
private void SetupMetaHooks()
{
OnModelLoadCompleteHook =
Hook< OnModelLoadCompleteDelegate >.FromAddress( DrawObjectHumanVTable[ OnModelLoadCompleteIdx ], OnModelLoadCompleteDetour );
}
private void EnableMetaHooks()
{
GetEqpIndirectHook?.Enable();
UpdateModelsHook?.Enable();
OnModelLoadCompleteHook?.Enable();
SetupVisorHook?.Enable();
RspSetupCharacterHook?.Enable();
}
private void DisableMetaHooks()
{
GetEqpIndirectHook?.Disable();
UpdateModelsHook?.Disable();
OnModelLoadCompleteHook?.Disable();
SetupVisorHook?.Disable();
RspSetupCharacterHook?.Disable();
}
private void DisposeMetaHooks()
{
GetEqpIndirectHook?.Dispose();
UpdateModelsHook?.Dispose();
OnModelLoadCompleteHook?.Dispose();
SetupVisorHook?.Dispose();
RspSetupCharacterHook?.Dispose();
}
private ModCollection? GetCollection( IntPtr drawObject )
{
var parent = FindParent( drawObject, out var collection );
if( parent == null || collection == Penumbra.CollectionManager.Default )
{
return null;
} }
return collection.HasCache ? collection : null; public void Dispose()
} {
_getEqpIndirectHook.Dispose();
_updateModelsHook.Dispose();
_onModelLoadCompleteHook.Dispose();
_setupVisorHook.Dispose();
_rspSetupCharacterHook.Dispose();
}
private delegate void OnModelLoadCompleteDelegate( IntPtr drawObject );
private readonly Hook< OnModelLoadCompleteDelegate > _onModelLoadCompleteHook;
private void OnModelLoadCompleteDetour( IntPtr drawObject )
{
var collection = GetCollection( drawObject );
if( collection != null )
{
using var eqp = MetaChanger.ChangeEqp( collection );
using var eqdp = MetaChanger.ChangeEqdp( collection );
_onModelLoadCompleteHook.Original.Invoke( drawObject );
}
else
{
_onModelLoadCompleteHook.Original.Invoke( drawObject );
}
}
private delegate void UpdateModelDelegate( IntPtr drawObject );
[Signature( "48 8B ?? 56 48 83 ?? ?? ?? B9", 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 + 0x90c ) == 0 )
{
return;
}
var collection = GetCollection( drawObject );
if( collection != null )
{
using var eqp = MetaChanger.ChangeEqp( collection );
using var eqdp = MetaChanger.ChangeEqdp( collection );
_updateModelsHook.Original.Invoke( drawObject );
}
else
{
_updateModelsHook.Original.Invoke( drawObject );
}
}
[Signature( "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C",
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 + 0xa30 ) & 1 ) == 0 || *( ulong* )( drawObject + 0xa28 ) == 0 )
{
return;
}
using var eqp = MetaChanger.ChangeEqp( _parent, drawObject );
_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( "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", DetourName = nameof( SetupVisorDetour ) )]
private readonly Hook< SetupVisorDelegate > _setupVisorHook = null!;
private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState )
{
using var gmp = MetaChanger.ChangeGmp( _parent, drawObject );
return _setupVisorHook.Original( drawObject, modelId, visorState );
}
// RSP
private delegate void RspSetupCharacterDelegate( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 );
[Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 88 54 24 ?? 57 41 56", DetourName = nameof( RspSetupCharacterDetour ) )]
private readonly Hook< RspSetupCharacterDelegate > _rspSetupCharacterHook = null!;
private void RspSetupCharacterDetour( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 )
{
using var rsp = MetaChanger.ChangeCmp( _parent, drawObject );
_rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 );
}
}
// Small helper to handle setting metadata and reverting it at the end of the function. // Small helper to handle setting metadata and reverting it at the end of the function.
// Since eqp and eqdp may be called multiple times in a row, we need to count them, // Since eqp and eqdp may be called multiple times in a row, we need to count them,
@ -194,11 +190,12 @@ public unsafe partial class PathResolver
public static MetaChanger ChangeEqp( PathResolver resolver, IntPtr drawObject ) public static MetaChanger ChangeEqp( PathResolver resolver, IntPtr drawObject )
{ {
var collection = resolver.GetCollection( drawObject ); var collection = GetCollection( drawObject );
if( collection != null ) if( collection != null )
{ {
return ChangeEqp( collection ); return ChangeEqp( collection );
} }
return new MetaChanger( MetaManipulation.Type.Unknown ); return new MetaChanger( MetaManipulation.Type.Unknown );
} }
@ -207,12 +204,13 @@ public unsafe partial class PathResolver
{ {
if( modelType < 10 ) if( modelType < 10 )
{ {
var collection = resolver.GetCollection( drawObject ); var collection = GetCollection( drawObject );
if( collection != null ) if( collection != null )
{ {
return ChangeEqdp( collection ); return ChangeEqdp( collection );
} }
} }
return new MetaChanger( MetaManipulation.Type.Unknown ); return new MetaChanger( MetaManipulation.Type.Unknown );
} }
@ -224,31 +222,33 @@ public unsafe partial class PathResolver
public static MetaChanger ChangeGmp( PathResolver resolver, IntPtr drawObject ) public static MetaChanger ChangeGmp( PathResolver resolver, IntPtr drawObject )
{ {
var collection = resolver.GetCollection( drawObject ); var collection = GetCollection( drawObject );
if( collection != null ) if( collection != null )
{ {
collection.SetGmpFiles(); collection.SetGmpFiles();
return new MetaChanger( MetaManipulation.Type.Gmp ); return new MetaChanger( MetaManipulation.Type.Gmp );
} }
return new MetaChanger( MetaManipulation.Type.Unknown ); return new MetaChanger( MetaManipulation.Type.Unknown );
} }
public static MetaChanger ChangeEst( PathResolver resolver, IntPtr drawObject ) public static MetaChanger ChangeEst( PathResolver resolver, IntPtr drawObject )
{ {
var collection = resolver.GetCollection( drawObject ); var collection = GetCollection( drawObject );
if( collection != null ) if( collection != null )
{ {
collection.SetEstFiles(); collection.SetEstFiles();
return new MetaChanger( MetaManipulation.Type.Est ); return new MetaChanger( MetaManipulation.Type.Est );
} }
return new MetaChanger( MetaManipulation.Type.Unknown ); return new MetaChanger( MetaManipulation.Type.Unknown );
} }
public static MetaChanger ChangeCmp( PathResolver resolver, out ModCollection? collection ) public static MetaChanger ChangeCmp( GameObject* gameObject, out ModCollection? collection )
{ {
if( resolver.LastGameObject != null ) if( gameObject != null )
{ {
collection = IdentifyCollection( resolver.LastGameObject ); collection = IdentifyCollection( gameObject );
if( collection != Penumbra.CollectionManager.Default && collection.HasCache ) if( collection != Penumbra.CollectionManager.Default && collection.HasCache )
{ {
collection.SetCmpFiles(); collection.SetCmpFiles();
@ -265,12 +265,13 @@ public unsafe partial class PathResolver
public static MetaChanger ChangeCmp( PathResolver resolver, IntPtr drawObject ) public static MetaChanger ChangeCmp( PathResolver resolver, IntPtr drawObject )
{ {
var collection = resolver.GetCollection( drawObject ); var collection = GetCollection( drawObject );
if( collection != null ) if( collection != null )
{ {
collection.SetCmpFiles(); collection.SetCmpFiles();
return new MetaChanger( MetaManipulation.Type.Rsp ); return new MetaChanger( MetaManipulation.Type.Rsp );
} }
return new MetaChanger( MetaManipulation.Type.Unknown ); return new MetaChanger( MetaManipulation.Type.Unknown );
} }

View file

@ -1,88 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
[Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83", ScanType = ScanType.StaticAddress )]
public IntPtr* DrawObjectMonsterVTable;
public Hook<GeneralResolveDelegate>? ResolveMonsterDecalPathHook;
public Hook<EidResolveDelegate>? ResolveMonsterEidPathHook;
public Hook<GeneralResolveDelegate>? ResolveMonsterImcPathHook;
public Hook<MPapResolveDelegate>? ResolveMonsterMPapPathHook;
public Hook<GeneralResolveDelegate>? ResolveMonsterMdlPathHook;
public Hook<MaterialResolveDetour>? ResolveMonsterMtrlPathHook;
public Hook<MaterialResolveDetour>? ResolveMonsterPapPathHook;
public Hook<GeneralResolveDelegate>? ResolveMonsterPhybPathHook;
public Hook<GeneralResolveDelegate>? ResolveMonsterSklbPathHook;
public Hook<GeneralResolveDelegate>? ResolveMonsterSkpPathHook;
public Hook<EidResolveDelegate>? ResolveMonsterTmbPathHook;
public Hook<MaterialResolveDetour>? ResolveMonsterVfxPathHook;
private void SetupMonsterHooks()
{
ResolveMonsterDecalPathHook = Hook<GeneralResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveDecalIdx], ResolveMonsterDecalDetour );
ResolveMonsterEidPathHook = Hook<EidResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveEidIdx], ResolveMonsterEidDetour );
ResolveMonsterImcPathHook = Hook<GeneralResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveImcIdx], ResolveMonsterImcDetour );
ResolveMonsterMPapPathHook = Hook<MPapResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveMPapIdx], ResolveMonsterMPapDetour );
ResolveMonsterMdlPathHook = Hook<GeneralResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveMdlIdx], ResolveMonsterMdlDetour );
ResolveMonsterMtrlPathHook = Hook<MaterialResolveDetour>.FromAddress( DrawObjectMonsterVTable[ResolveMtrlIdx], ResolveMonsterMtrlDetour );
ResolveMonsterPapPathHook = Hook<MaterialResolveDetour>.FromAddress( DrawObjectMonsterVTable[ResolvePapIdx], ResolveMonsterPapDetour );
ResolveMonsterPhybPathHook = Hook<GeneralResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolvePhybIdx], ResolveMonsterPhybDetour );
ResolveMonsterSklbPathHook = Hook<GeneralResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveSklbIdx], ResolveMonsterSklbDetour );
ResolveMonsterSkpPathHook = Hook<GeneralResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveSkpIdx], ResolveMonsterSkpDetour );
ResolveMonsterTmbPathHook = Hook<EidResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveTmbIdx], ResolveMonsterTmbDetour );
ResolveMonsterVfxPathHook = Hook<MaterialResolveDetour>.FromAddress( DrawObjectMonsterVTable[ResolveVfxIdx], ResolveMonsterVfxDetour );
}
private void EnableMonsterHooks()
{
ResolveMonsterDecalPathHook?.Enable();
ResolveMonsterEidPathHook?.Enable();
ResolveMonsterImcPathHook?.Enable();
ResolveMonsterMPapPathHook?.Enable();
ResolveMonsterMdlPathHook?.Enable();
ResolveMonsterMtrlPathHook?.Enable();
ResolveMonsterPapPathHook?.Enable();
ResolveMonsterPhybPathHook?.Enable();
ResolveMonsterSklbPathHook?.Enable();
ResolveMonsterSkpPathHook?.Enable();
ResolveMonsterTmbPathHook?.Enable();
ResolveMonsterVfxPathHook?.Enable();
}
private void DisableMonsterHooks()
{
ResolveMonsterDecalPathHook?.Disable();
ResolveMonsterEidPathHook?.Disable();
ResolveMonsterImcPathHook?.Disable();
ResolveMonsterMPapPathHook?.Disable();
ResolveMonsterMdlPathHook?.Disable();
ResolveMonsterMtrlPathHook?.Disable();
ResolveMonsterPapPathHook?.Disable();
ResolveMonsterPhybPathHook?.Disable();
ResolveMonsterSklbPathHook?.Disable();
ResolveMonsterSkpPathHook?.Disable();
ResolveMonsterTmbPathHook?.Disable();
ResolveMonsterVfxPathHook?.Disable();
}
private void DisposeMonsterHooks()
{
ResolveMonsterDecalPathHook?.Dispose();
ResolveMonsterEidPathHook?.Dispose();
ResolveMonsterImcPathHook?.Dispose();
ResolveMonsterMPapPathHook?.Dispose();
ResolveMonsterMdlPathHook?.Dispose();
ResolveMonsterMtrlPathHook?.Dispose();
ResolveMonsterPapPathHook?.Dispose();
ResolveMonsterPhybPathHook?.Dispose();
ResolveMonsterSklbPathHook?.Dispose();
ResolveMonsterSkpPathHook?.Dispose();
ResolveMonsterTmbPathHook?.Dispose();
ResolveMonsterVfxPathHook?.Dispose();
}
}

View file

@ -0,0 +1,109 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
public class PathState : IDisposable
{
[Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1", ScanType = ScanType.StaticAddress )]
public readonly IntPtr* HumanVTable = null!;
[Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B",
ScanType = ScanType.StaticAddress )]
private readonly IntPtr* _weaponVTable = null!;
[Signature( "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA", ScanType = ScanType.StaticAddress )]
private readonly IntPtr* _demiHumanVTable = null!;
[Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83", 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< Utf8String, ModCollection > _pathCollections = new();
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()
{
_human.Dispose();
_weapon.Dispose();
_demiHuman.Dispose();
_monster.Dispose();
}
public int Count
=> _pathCollections.Count;
public IEnumerable< KeyValuePair< Utf8String, ModCollection > > Paths
=> _pathCollections;
public bool TryGetValue( Utf8String path, [NotNullWhen( true )] out ModCollection? collection )
=> _pathCollections.TryGetValue( path, out collection );
public bool Consume( Utf8String path, [NotNullWhen( true )] out ModCollection? collection )
=> _pathCollections.TryRemove( path, out collection );
// Just add or remove the resolved path.
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
public IntPtr ResolvePath( ModCollection collection, IntPtr path )
{
if( path == IntPtr.Zero )
{
return path;
}
var gamePath = new Utf8String( ( byte* )path );
SetCollection( gamePath, collection );
return path;
}
// Special handling for paths so that we do not store non-owned temporary strings in the dictionary.
public void SetCollection( Utf8String path, ModCollection collection )
{
if( _pathCollections.ContainsKey( path ) || path.IsOwned )
{
_pathCollections[ path ] = collection;
}
else
{
_pathCollections[ path.Clone() ] = collection;
}
}
}
}

View file

@ -1,223 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
namespace Penumbra.Interop.Resolver;
// The actual resolve detours are basically all the same.
public unsafe partial class PathResolver
{
// Humans
private IntPtr ResolveDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveDecalPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePathDetour( drawObject, ResolveEidPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveImcPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 )
=> ResolvePathDetour( drawObject, ResolveMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
{
using var eqdp = MetaChanger.ChangeEqdp( this, drawObject, modelType );
return ResolvePathDetour( drawObject, ResolveMdlPathHook!.Original( drawObject, path, unk3, modelType ) );
}
private IntPtr ResolveMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolvePapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
{
using var est = MetaChanger.ChangeEst( this, drawObject );
return ResolvePathDetour( drawObject, ResolvePapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
}
private IntPtr ResolvePhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = MetaChanger.ChangeEst( this, drawObject );
return ResolvePathDetour( drawObject, ResolvePhybPathHook!.Original( drawObject, path, unk3, unk4 ) );
}
private IntPtr ResolveSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = MetaChanger.ChangeEst( this, drawObject );
return ResolvePathDetour( drawObject, ResolveSklbPathHook!.Original( drawObject, path, unk3, unk4 ) );
}
private IntPtr ResolveSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = MetaChanger.ChangeEst( this, drawObject );
return ResolvePathDetour( drawObject, ResolveSkpPathHook!.Original( drawObject, path, unk3, unk4 ) );
}
private IntPtr ResolveTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePathDetour( drawObject, ResolveTmbPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
// Weapons
private IntPtr ResolveWeaponDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponDecalPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveWeaponEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponEidPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveWeaponImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponImcPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveWeaponMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveWeaponMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponMdlPathHook!.Original( drawObject, path, unk3, modelType ) );
private IntPtr ResolveWeaponMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveWeaponPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveWeaponPhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponPhybPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveWeaponSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponSklbPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveWeaponSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponSkpPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveWeaponTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponTmbPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveWeaponVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
// Monsters
private IntPtr ResolveMonsterDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveMonsterDecalPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMonsterEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePathDetour( drawObject, ResolveMonsterEidPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveMonsterImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveMonsterImcPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMonsterMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 )
=> ResolvePathDetour( drawObject, ResolveMonsterMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMonsterMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
=> ResolvePathDetour( drawObject, ResolveMonsterMdlPathHook!.Original( drawObject, path, unk3, modelType ) );
private IntPtr ResolveMonsterMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveMonsterMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMonsterPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveMonsterPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMonsterPhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveMonsterPhybPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMonsterSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveMonsterSklbPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMonsterSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveMonsterSkpPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMonsterTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePathDetour( drawObject, ResolveMonsterTmbPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveMonsterVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveMonsterVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
// Demihumans
private IntPtr ResolveDemiDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveDemiDecalPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveDemiEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePathDetour( drawObject, ResolveDemiEidPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveDemiImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveDemiImcPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveDemiMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 )
=> ResolvePathDetour( drawObject, ResolveDemiMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveDemiMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
=> ResolvePathDetour( drawObject, ResolveDemiMdlPathHook!.Original( drawObject, path, unk3, modelType ) );
private IntPtr ResolveDemiMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveDemiMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveDemiPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveDemiPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveDemiPhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveDemiPhybPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveDemiSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveDemiSklbPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveDemiSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveDemiSkpPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveDemiTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePathDetour( drawObject, ResolveDemiTmbPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveDemiVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveDemiVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
// Implementation
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path )
=> ResolvePathDetour( FindParent( drawObject, out var collection ) == null
? Penumbra.CollectionManager.Default
: collection, 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 ResolveWeaponPathDetour( IntPtr drawObject, IntPtr path )
{
var parent = FindParent( drawObject, out var collection );
if( parent != null )
{
return ResolvePathDetour( collection, path );
}
var parentObject = ( ( DrawObject* )drawObject )->Object.ParentObject;
if( parentObject == null && LastGameObject != null )
{
var c2 = IdentifyCollection( LastGameObject );
DrawObjectToObject[ drawObject ] = ( c2, LastGameObject->ObjectIndex );
return ResolvePathDetour( c2, path );
}
parent = FindParent( ( IntPtr )parentObject, out collection );
return ResolvePathDetour( parent == null
? Penumbra.CollectionManager.Default
: collection, path );
}
// Just add or remove the resolved path.
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
private IntPtr ResolvePathDetour( ModCollection collection, IntPtr path )
{
if( path == IntPtr.Zero )
{
return path;
}
var gamePath = new Utf8String( ( byte* )path );
SetCollection( gamePath, collection );
return path;
}
}

View file

@ -0,0 +1,258 @@
using System;
using System.Runtime.CompilerServices;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
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 )
{
using var eqdp = MetaChanger.ChangeEqdp( _parent, drawObject, modelType );
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 = MetaChanger.ChangeEst( _parent, 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 = MetaChanger.ChangeEst( _parent, drawObject );
return ResolvePath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) );
}
private IntPtr ResolveSklbHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = MetaChanger.ChangeEst( _parent, drawObject );
return ResolvePath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) );
}
private IntPtr ResolveSkpHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = MetaChanger.ChangeEst( _parent, drawObject );
return ResolvePath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) );
}
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( FindParent( drawObject, out var collection ) == null
? Penumbra.CollectionManager.Default
: collection, 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( collection, path );
}
var parentObject = ( IntPtr )( ( DrawObject* )drawObject )->Object.ParentObject;
var parentCollection = DrawObjects.CheckParentDrawObject( drawObject, parentObject );
if( parentCollection != null )
{
return _parent._paths.ResolvePath( parentCollection, path );
}
parent = FindParent( parentObject, out collection );
return _parent._paths.ResolvePath( parent == null
? Penumbra.CollectionManager.Default
: collection, path );
}
}
}

View file

@ -1,89 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
[Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B",
ScanType = ScanType.StaticAddress )]
public IntPtr* DrawObjectWeaponVTable;
public Hook< GeneralResolveDelegate >? ResolveWeaponDecalPathHook;
public Hook< EidResolveDelegate >? ResolveWeaponEidPathHook;
public Hook< GeneralResolveDelegate >? ResolveWeaponImcPathHook;
public Hook< MPapResolveDelegate >? ResolveWeaponMPapPathHook;
public Hook< GeneralResolveDelegate >? ResolveWeaponMdlPathHook;
public Hook< MaterialResolveDetour >? ResolveWeaponMtrlPathHook;
public Hook< MaterialResolveDetour >? ResolveWeaponPapPathHook;
public Hook< GeneralResolveDelegate >? ResolveWeaponPhybPathHook;
public Hook< GeneralResolveDelegate >? ResolveWeaponSklbPathHook;
public Hook< GeneralResolveDelegate >? ResolveWeaponSkpPathHook;
public Hook< EidResolveDelegate >? ResolveWeaponTmbPathHook;
public Hook< MaterialResolveDetour >? ResolveWeaponVfxPathHook;
private void SetupWeaponHooks()
{
ResolveWeaponDecalPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveDecalIdx ], ResolveWeaponDecalDetour );
ResolveWeaponEidPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveEidIdx ], ResolveWeaponEidDetour );
ResolveWeaponImcPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveImcIdx ], ResolveWeaponImcDetour );
ResolveWeaponMPapPathHook = Hook< MPapResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveMPapIdx ], ResolveWeaponMPapDetour );
ResolveWeaponMdlPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveMdlIdx ], ResolveWeaponMdlDetour );
ResolveWeaponMtrlPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectWeaponVTable[ ResolveMtrlIdx ], ResolveWeaponMtrlDetour );
ResolveWeaponPapPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectWeaponVTable[ ResolvePapIdx ], ResolveWeaponPapDetour );
ResolveWeaponPhybPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolvePhybIdx ], ResolveWeaponPhybDetour );
ResolveWeaponSklbPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveSklbIdx ], ResolveWeaponSklbDetour );
ResolveWeaponSkpPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveSkpIdx ], ResolveWeaponSkpDetour );
ResolveWeaponTmbPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveTmbIdx ], ResolveWeaponTmbDetour );
ResolveWeaponVfxPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectWeaponVTable[ ResolveVfxIdx ], ResolveWeaponVfxDetour );
}
private void EnableWeaponHooks()
{
ResolveWeaponDecalPathHook?.Enable();
ResolveWeaponEidPathHook?.Enable();
ResolveWeaponImcPathHook?.Enable();
ResolveWeaponMPapPathHook?.Enable();
ResolveWeaponMdlPathHook?.Enable();
ResolveWeaponMtrlPathHook?.Enable();
ResolveWeaponPapPathHook?.Enable();
ResolveWeaponPhybPathHook?.Enable();
ResolveWeaponSklbPathHook?.Enable();
ResolveWeaponSkpPathHook?.Enable();
ResolveWeaponTmbPathHook?.Enable();
ResolveWeaponVfxPathHook?.Enable();
}
private void DisableWeaponHooks()
{
ResolveWeaponDecalPathHook?.Disable();
ResolveWeaponEidPathHook?.Disable();
ResolveWeaponImcPathHook?.Disable();
ResolveWeaponMPapPathHook?.Disable();
ResolveWeaponMdlPathHook?.Disable();
ResolveWeaponMtrlPathHook?.Disable();
ResolveWeaponPapPathHook?.Disable();
ResolveWeaponPhybPathHook?.Disable();
ResolveWeaponSklbPathHook?.Disable();
ResolveWeaponSkpPathHook?.Disable();
ResolveWeaponTmbPathHook?.Disable();
ResolveWeaponVfxPathHook?.Disable();
}
private void DisposeWeaponHooks()
{
ResolveWeaponDecalPathHook?.Dispose();
ResolveWeaponEidPathHook?.Dispose();
ResolveWeaponImcPathHook?.Dispose();
ResolveWeaponMPapPathHook?.Dispose();
ResolveWeaponMdlPathHook?.Dispose();
ResolveWeaponMtrlPathHook?.Dispose();
ResolveWeaponPapPathHook?.Dispose();
ResolveWeaponPhybPathHook?.Dispose();
ResolveWeaponSklbPathHook?.Dispose();
ResolveWeaponSkpPathHook?.Dispose();
ResolveWeaponTmbPathHook?.Dispose();
ResolveWeaponVfxPathHook?.Dispose();
}
}

View file

@ -1,7 +1,9 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Collections.Generic;
using Dalamud.Logging; using Dalamud.Logging;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
@ -17,18 +19,24 @@ namespace Penumbra.Interop.Resolver;
// to resolve paths for character collections. // to resolve paths for character collections.
public partial class PathResolver : IDisposable public partial class PathResolver : IDisposable
{ {
private readonly ResourceLoader _loader;
public bool Enabled { get; private set; } public bool Enabled { get; private set; }
public PathResolver( ResourceLoader loader ) private readonly ResourceLoader _loader;
private static readonly CutsceneCharacters Cutscenes = new();
private static readonly DrawObjectState DrawObjects = new();
private readonly AnimationState _animations;
private readonly PathState _paths;
private readonly MetaState _meta;
private readonly MaterialState _materials;
public unsafe PathResolver( ResourceLoader loader )
{ {
_loader = loader;
SignatureHelper.Initialise( this ); SignatureHelper.Initialise( this );
SetupHumanHooks(); _loader = loader;
SetupWeaponHooks(); _animations = new AnimationState( DrawObjects );
SetupMonsterHooks(); _paths = new PathState( this );
SetupDemiHooks(); _meta = new MetaState( this, _paths.HumanVTable );
SetupMetaHooks(); _materials = new MaterialState( _paths );
} }
// The modified resolver that handles game path resolving. // The modified resolver that handles game path resolving.
@ -40,10 +48,10 @@ public partial class PathResolver : IDisposable
// If not use the default collection. // If not use the default collection.
// We can remove paths after they have actually been loaded. // We can remove paths after they have actually been loaded.
// A potential next request will add the path anew. // A potential next request will add the path anew.
var nonDefault = HandleMaterialSubFiles( type, out var collection ) var nonDefault = _materials.HandleSubFiles( type, out var collection )
|| PathCollections.TryRemove( gamePath.Path, out collection ) || _paths.Consume( gamePath.Path, out collection )
|| HandleAnimationFile( type, gamePath, out collection ) || _animations.HandleFiles( type, gamePath, out collection )
|| HandleDecalFile( type, gamePath, out collection ); || DrawObjects.HandleDecalFile( type, gamePath, out collection );
if( !nonDefault || collection == null ) if( !nonDefault || collection == null )
{ {
collection = Penumbra.CollectionManager.Default; collection = Penumbra.CollectionManager.Default;
@ -56,67 +64,10 @@ public partial class PathResolver : IDisposable
// so that the functions loading tex and shpk can find that path and use its collection. // so that the functions loading tex and shpk can find that path and use its collection.
// We also need to handle defaulted materials against a non-default collection. // We also need to handle defaulted materials against a non-default collection.
var path = resolved == null ? gamePath.Path.ToString() : resolved.Value.FullName; var path = resolved == null ? gamePath.Path.ToString() : resolved.Value.FullName;
HandleMtrlCollection( collection, path, nonDefault, type, resolved, out data ); MaterialState.HandleCollection( collection, path, nonDefault, type, resolved, out data );
return true; return true;
} }
private bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, [NotNullWhen( true )] out ModCollection? collection )
{
if( type == ResourceType.Tex
&& _lastCreatedCollection != null
&& gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l', '_', 'f', 'a', 'c', 'e' ) )
{
collection = _lastCreatedCollection;
return true;
}
collection = null;
return false;
}
private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, [NotNullWhen( true )] out ModCollection? collection )
{
switch( type )
{
case ResourceType.Tmb:
case ResourceType.Pap:
case ResourceType.Scd:
if( _animationLoadCollection != null )
{
collection = _animationLoadCollection;
return true;
}
break;
case ResourceType.Avfx:
_lastAvfxCollection = _animationLoadCollection ?? Penumbra.CollectionManager.Default;
if( _animationLoadCollection != null )
{
collection = _animationLoadCollection;
return true;
}
break;
case ResourceType.Atex:
if( _lastAvfxCollection != null )
{
collection = _lastAvfxCollection;
return true;
}
if( _animationLoadCollection != null )
{
collection = _animationLoadCollection;
return true;
}
break;
}
collection = null;
return false;
}
public void Enable() public void Enable()
{ {
if( Enabled ) if( Enabled )
@ -125,15 +76,12 @@ public partial class PathResolver : IDisposable
} }
Enabled = true; Enabled = true;
InitializeDrawObjects(); Cutscenes.Enable();
DrawObjects.Enable();
EnableHumanHooks(); _animations.Enable();
EnableWeaponHooks(); _paths.Enable();
EnableMonsterHooks(); _meta.Enable();
EnableDemiHooks(); _materials.Enable();
EnableMtrlHooks();
EnableDataHooks();
EnableMetaHooks();
_loader.ResolvePathCustomization += CharacterResolver; _loader.ResolvePathCustomization += CharacterResolver;
PluginLog.Debug( "Character Path Resolver enabled." ); PluginLog.Debug( "Character Path Resolver enabled." );
@ -147,16 +95,12 @@ public partial class PathResolver : IDisposable
} }
Enabled = false; Enabled = false;
DisableHumanHooks(); _animations.Disable();
DisableWeaponHooks(); DrawObjects.Disable();
DisableMonsterHooks(); Cutscenes.Disable();
DisableDemiHooks(); _paths.Disable();
DisableMtrlHooks(); _meta.Disable();
DisableDataHooks(); _materials.Disable();
DisableMetaHooks();
DrawObjectToObject.Clear();
PathCollections.Clear();
_loader.ResolvePathCustomization -= CharacterResolver; _loader.ResolvePathCustomization -= CharacterResolver;
PluginLog.Debug( "Character Path Resolver disabled." ); PluginLog.Debug( "Character Path Resolver disabled." );
@ -165,18 +109,60 @@ public partial class PathResolver : IDisposable
public void Dispose() public void Dispose()
{ {
Disable(); Disable();
DisposeHumanHooks(); _paths.Dispose();
DisposeWeaponHooks(); _animations.Dispose();
DisposeMonsterHooks(); DrawObjects.Dispose();
DisposeDemiHooks(); Cutscenes.Dispose();
DisposeMtrlHooks(); _meta.Dispose();
DisposeDataHooks(); _materials.Dispose();
DisposeMetaHooks();
} }
public unsafe (IntPtr, ModCollection) IdentifyDrawObject( IntPtr drawObject ) public static unsafe (IntPtr, ModCollection) IdentifyDrawObject( IntPtr drawObject )
{ {
var parent = FindParent( drawObject, out var collection ); var parent = FindParent( drawObject, out var collection );
return ( ( IntPtr )parent, collection ); return ( ( IntPtr )parent, collection );
} }
public int CutsceneActor( int idx )
=> Cutscenes.GetParentIndex( idx );
// Use the stored information to find the GameObject and Collection linked to a DrawObject.
public static unsafe GameObject* FindParent( IntPtr drawObject, out ModCollection collection )
{
if( DrawObjects.TryGetValue( drawObject, out var data, out var gameObject ) )
{
collection = data.Item1;
return gameObject;
}
if( DrawObjects.LastGameObject != null
&& ( DrawObjects.LastGameObject->DrawObject == null || DrawObjects.LastGameObject->DrawObject == ( DrawObject* )drawObject ) )
{
collection = IdentifyCollection( DrawObjects.LastGameObject );
return DrawObjects.LastGameObject;
}
collection = IdentifyCollection( null );
return null;
}
private static unsafe ModCollection? GetCollection( IntPtr drawObject )
{
var parent = FindParent( drawObject, out var collection );
if( parent == null || collection == Penumbra.CollectionManager.Default )
{
return null;
}
return collection.HasCache ? collection : null;
}
internal IEnumerable< KeyValuePair< Utf8String, ModCollection > > PathCollections
=> _paths.Paths;
internal IEnumerable< KeyValuePair< IntPtr, (ModCollection, int) > > DrawObjectMap
=> DrawObjects.DrawObjects;
internal IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > CutsceneActors
=> Cutscenes.Actors;
} }

View file

@ -11,6 +11,7 @@ using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.Interop.Loader; using Penumbra.Interop.Loader;
using Penumbra.Interop.Resolver;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using CharacterUtility = Penumbra.Interop.CharacterUtility; using CharacterUtility = Penumbra.Interop.CharacterUtility;
@ -155,45 +156,63 @@ public partial class ConfigWindow
return; return;
} }
using var drawTree = ImRaii.TreeNode( "Draw Object to Object" ); using( var drawTree = ImRaii.TreeNode( "Draw Object to Object" ) )
if( drawTree )
{ {
using var table = ImRaii.Table( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit ); if( drawTree )
if( table )
{ {
foreach( var (ptr, (c, idx)) in _window._penumbra.PathResolver.DrawObjectToObject ) using var table = ImRaii.Table( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit );
if( table )
{ {
ImGui.TableNextColumn(); foreach( var (ptr, (c, idx)) in _window._penumbra.PathResolver.DrawObjectMap )
ImGui.TextUnformatted( ptr.ToString( "X" ) ); {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted( idx.ToString() ); ImGui.TextUnformatted( ptr.ToString( "X" ) );
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var obj = ( GameObject* )Dalamud.Objects.GetObjectAddress( idx ); ImGui.TextUnformatted( idx.ToString() );
var (address, name) = ImGui.TableNextColumn();
obj != null ? ( $"0x{( ulong )obj:X}", new Utf8String( obj->Name ).ToString() ) : ( "NULL", "NULL" ); var obj = ( GameObject* )Dalamud.Objects.GetObjectAddress( idx );
ImGui.TextUnformatted( address ); var (address, name) =
ImGui.TableNextColumn(); obj != null ? ( $"0x{( ulong )obj:X}", new Utf8String( obj->Name ).ToString() ) : ( "NULL", "NULL" );
ImGui.TextUnformatted( name ); ImGui.TextUnformatted( address );
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted( c.Name ); ImGui.TextUnformatted( name );
ImGui.TableNextColumn();
ImGui.TextUnformatted( c.Name );
}
} }
} }
} }
drawTree.Dispose(); using( var pathTree = ImRaii.TreeNode( "Path Collections" ) )
using var pathTree = ImRaii.TreeNode( "Path Collections" );
if( pathTree )
{ {
using var table = ImRaii.Table( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); if( pathTree )
{
using var table = ImRaii.Table( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit );
if( table )
{
foreach( var (path, collection) in _window._penumbra.PathResolver.PathCollections )
{
ImGui.TableNextColumn();
ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length );
ImGui.TableNextColumn();
ImGui.TextUnformatted( collection.Name );
}
}
}
}
using var cutsceneTree = ImRaii.TreeNode( "Cutscene Actors" );
if( cutsceneTree )
{
using var table = ImRaii.Table( "###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit );
if( table ) if( table )
{ {
foreach( var (path, collection) in _window._penumbra.PathResolver.PathCollections ) foreach( var (idx, actor) in _window._penumbra.PathResolver.CutsceneActors )
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); ImGui.TextUnformatted( $"Cutscene Actor {idx}" );
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted( collection.Name ); ImGui.TextUnformatted( actor.Name.ToString() );
} }
} }
} }