From 24fda725a2992c65765c57a63e77db155d16f468 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 22 Jan 2023 15:16:53 +0100 Subject: [PATCH] Add GameEventManager, change cutscene character and subfile container resets. --- Penumbra.GameData/Signatures.cs | 9 +- Penumbra/Interop/GameEventManager.cs | 126 ++++++++++++++++++ .../Interop/Resolver/CutsceneCharacters.cs | 90 ++++--------- .../Resolver/IdentifiedCollectionCache.cs | 29 ++-- .../Interop/Resolver/PathResolver.Subfiles.cs | 22 ++- Penumbra/Interop/Resolver/PathResolver.cs | 9 +- Penumbra/Penumbra.cs | 5 +- 7 files changed, 183 insertions(+), 107 deletions(-) create mode 100644 Penumbra/Interop/GameEventManager.cs diff --git a/Penumbra.GameData/Signatures.cs b/Penumbra.GameData/Signatures.cs index fd6bdcfd..5b7f139b 100644 --- a/Penumbra.GameData/Signatures.cs +++ b/Penumbra.GameData/Signatures.cs @@ -3,8 +3,7 @@ namespace Penumbra.GameData; public static class Sigs { // ResourceLoader.Debug - public const string ResourceHandleDestructor = "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8"; - public const string ResourceManager = "48 8B 05 ?? ?? ?? ?? 33 ED F0"; + public const string ResourceManager = "48 8B 05 ?? ?? ?? ?? 33 ED F0"; // ResourceLoader.Replacement public const string GetResourceSync = "E8 ?? ?? 00 00 48 8D 8F ?? ?? 00 00 48 89 87 ?? ?? 00 00"; @@ -19,10 +18,10 @@ public static class Sigs public const string LoadTexFileExtern = "E8 ?? ?? ?? ?? 0F B6 E8 48 8B CB E8"; public const string LoadMdlFileExtern = "E8 ?? ?? ?? ?? EB 02 B0 F1"; - // CutsceneCharacters - public const string CopyCharacter = "E8 ?? ?? ?? ?? 0F B6 9F ?? ?? ?? ?? 48 8D 8F"; + // GameEventManager + public const string ResourceHandleDestructor = "48 89 5C 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 B8"; + public const string CopyCharacter = "E8 ?? ?? ?? ?? 0F B6 9F ?? ?? ?? ?? 48 8D 8F"; - // IdentifiedCollectionCache public const string CharacterDestructor = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8D 05"; diff --git a/Penumbra/Interop/GameEventManager.cs b/Penumbra/Interop/GameEventManager.cs new file mode 100644 index 00000000..1549f888 --- /dev/null +++ b/Penumbra/Interop/GameEventManager.cs @@ -0,0 +1,126 @@ +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using Penumbra.GameData; +using System; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop; + +public unsafe class GameEventManager : IDisposable +{ + public GameEventManager() + { + SignatureHelper.Initialise( this ); + _characterDtorHook.Enable(); + _copyCharacterHook.Enable(); + _resourceHandleDestructorHook.Enable(); + } + + public void Dispose() + { + _characterDtorHook.Dispose(); + _copyCharacterHook.Dispose(); + _resourceHandleDestructorHook.Dispose(); + } + + #region Character Destructor + + private delegate void CharacterDestructorDelegate( Character* character ); + + [Signature( Sigs.CharacterDestructor, DetourName = nameof( CharacterDestructorDetour ) )] + private readonly Hook< CharacterDestructorDelegate > _characterDtorHook = null!; + + private void CharacterDestructorDetour( Character* character ) + { + if( CharacterDestructor != null ) + { + foreach( var subscriber in CharacterDestructor.GetInvocationList() ) + { + try + { + ( ( CharacterDestructorEvent )subscriber ).Invoke( character ); + } + catch( Exception ex ) + { + Penumbra.Log.Error( $"Error in {nameof( CharacterDestructor )} event when executing {subscriber.Method.Name}:\n{ex}" ); + } + } + } + + Penumbra.Log.Verbose( $"{nameof( CharacterDestructor )} triggered with 0x{( nint )character:X}." ); + _characterDtorHook.Original( character ); + } + + public delegate void CharacterDestructorEvent( Character* character ); + public event CharacterDestructorEvent? CharacterDestructor; + + #endregion + + #region Copy Character + + private unsafe delegate ulong CopyCharacterDelegate( GameObject* target, GameObject* source, uint unk ); + + [Signature( Sigs.CopyCharacter, DetourName = nameof( CopyCharacterDetour ) )] + private readonly Hook< CopyCharacterDelegate > _copyCharacterHook = null!; + + private ulong CopyCharacterDetour( GameObject* target, GameObject* source, uint unk ) + { + if( CopyCharacter != null ) + { + foreach( var subscriber in CopyCharacter.GetInvocationList() ) + { + try + { + ( ( CopyCharacterEvent )subscriber ).Invoke( ( Character* )target, ( Character* )source ); + } + catch( Exception ex ) + { + Penumbra.Log.Error( $"Error in {nameof( CopyCharacter )} event when executing {subscriber.Method.Name}:\n{ex}" ); + } + } + } + + Penumbra.Log.Verbose( $"{nameof( CopyCharacter )} triggered with target 0x{( nint )target:X} and source 0x{( nint )source:X}." ); + return _copyCharacterHook.Original( target, source, unk ); + } + + public delegate void CopyCharacterEvent( Character* target, Character* source ); + public event CopyCharacterEvent? CopyCharacter; + + #endregion + + #region ResourceHandle Destructor + + private delegate IntPtr ResourceHandleDestructorDelegate( ResourceHandle* handle ); + + [Signature( Sigs.ResourceHandleDestructor, DetourName = nameof( ResourceHandleDestructorDetour ) )] + private readonly Hook< ResourceHandleDestructorDelegate > _resourceHandleDestructorHook = null!; + + private IntPtr ResourceHandleDestructorDetour( ResourceHandle* handle ) + { + if( ResourceHandleDestructor != null ) + { + foreach( var subscriber in ResourceHandleDestructor.GetInvocationList() ) + { + try + { + ( ( ResourceHandleDestructorEvent )subscriber ).Invoke( handle ); + } + catch( Exception ex ) + { + Penumbra.Log.Error( $"Error in {nameof( ResourceHandleDestructor )} event when executing {subscriber.Method.Name}:\n{ex}" ); + } + } + } + + Penumbra.Log.Verbose( $"{nameof( ResourceHandleDestructor )} triggered with 0x{( nint )handle:X}." ); + return _resourceHandleDestructorHook!.Original( handle ); + } + + public delegate void ResourceHandleDestructorEvent( ResourceHandle* handle ); + public event ResourceHandleDestructorEvent? ResourceHandleDestructor; + + #endregion +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/CutsceneCharacters.cs b/Penumbra/Interop/Resolver/CutsceneCharacters.cs index 7a3b9cf7..a1e2961b 100644 --- a/Penumbra/Interop/Resolver/CutsceneCharacters.cs +++ b/Penumbra/Interop/Resolver/CutsceneCharacters.cs @@ -2,11 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Penumbra.GameData; +using FFXIVClientStructs.FFXIV.Client.Game.Character; namespace Penumbra.Interop.Resolver; @@ -16,17 +12,18 @@ public class CutsceneCharacters : IDisposable public const int CutsceneSlots = 40; public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots; - private readonly short[] _copiedCharacters = Enumerable.Repeat( ( short )-1, CutsceneSlots ).ToArray(); + private readonly GameEventManager _events; + private readonly short[] _copiedCharacters = Enumerable.Repeat( ( short )-1, 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() + public CutsceneCharacters(GameEventManager events) { - SignatureHelper.Initialise( this ); - Dalamud.Conditions.ConditionChange += Reset; + _events = events; + Enable(); } // Get the related actor to a cutscene actor. @@ -53,71 +50,36 @@ public class CutsceneCharacters : IDisposable return -1; } - public void Reset( ConditionFlag flag, bool value ) + public unsafe void Enable() { - switch( flag ) - { - case ConditionFlag.BetweenAreas: - case ConditionFlag.BetweenAreas51: - if( !value ) - { - return; - } - - break; - case ConditionFlag.OccupiedInCutSceneEvent: - case ConditionFlag.WatchingCutscene: - case ConditionFlag.WatchingCutscene78: - if( value ) - { - return; - } - - break; - default: return; - } - - for( var i = 0; i < _copiedCharacters.Length; ++i ) - { - _copiedCharacters[ i ] = -1; - } + _events.CopyCharacter += OnCharacterCopy; + _events.CharacterDestructor += OnCharacterDestructor; } - public void Enable() - => _copyCharacterHook.Enable(); - - public void Disable() - => _copyCharacterHook.Disable(); + public unsafe void Disable() + { + _events.CopyCharacter -= OnCharacterCopy; + _events.CharacterDestructor -= OnCharacterDestructor; + } public void Dispose() + => Disable(); + + private unsafe void OnCharacterDestructor( Character* character ) { - _copyCharacterHook.Dispose(); - Dalamud.Conditions.ConditionChange -= Reset; + if( character->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx ) + { + var idx = character->GameObject.ObjectIndex - CutsceneStartIdx; + _copiedCharacters[ idx ] = -1; + } } - private unsafe delegate ulong CopyCharacterDelegate( GameObject* target, GameObject* source, uint unk ); - - [Signature( Sigs.CopyCharacter, DetourName = nameof( CopyCharacterDetour ) )] - private readonly Hook< CopyCharacterDelegate > _copyCharacterHook = null!; - - private unsafe ulong CopyCharacterDetour( GameObject* target, GameObject* source, uint unk ) + private unsafe void OnCharacterCopy( Character* target, Character* source ) { - try + if( target != null && target->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx ) { - 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; - Penumbra.Log.Debug( $"Set cutscene character {target->ObjectIndex} to {parent}." ); - } + var idx = target->GameObject.ObjectIndex - CutsceneStartIdx; + _copiedCharacters[idx] = (short) (source != null ? source->GameObject.ObjectIndex : -1); } - catch - { - // ignored - } - - return _copyCharacterHook.Original( target, source, unk ); } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs index 12fd6749..b3ffc5d3 100644 --- a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs @@ -1,25 +1,23 @@ using System; using System.Collections; using System.Collections.Generic; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; -using Penumbra.GameData; using Penumbra.GameData.Actors; namespace Penumbra.Interop.Resolver; public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPtr Address, ActorIdentifier Identifier, ModCollection Collection) > { + private readonly GameEventManager _events; private readonly Dictionary< IntPtr, (ActorIdentifier, ModCollection) > _cache = new(317); private bool _dirty = false; private bool _enabled = false; - public IdentifiedCollectionCache() + public IdentifiedCollectionCache(GameEventManager events) { - SignatureHelper.Initialise( this ); + _events = events; } public void Enable() @@ -32,8 +30,8 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt Penumbra.CollectionManager.CollectionChanged += CollectionChangeClear; Penumbra.TempMods.CollectionChanged += CollectionChangeClear; Dalamud.ClientState.TerritoryChanged += TerritoryClear; - _characterDtorHook.Enable(); - _enabled = true; + _events.CharacterDestructor += OnCharacterDestruct; + _enabled = true; } public void Disable() @@ -46,8 +44,8 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt Penumbra.CollectionManager.CollectionChanged -= CollectionChangeClear; Penumbra.TempMods.CollectionChanged -= CollectionChangeClear; Dalamud.ClientState.TerritoryChanged -= TerritoryClear; - _characterDtorHook.Disable(); - _enabled = false; + _events.CharacterDestructor -= OnCharacterDestruct; + _enabled = false; } public ResolveData Set( ModCollection collection, ActorIdentifier identifier, GameObject* data ) @@ -82,7 +80,6 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt public void Dispose() { Disable(); - _characterDtorHook.Dispose(); GC.SuppressFinalize( this ); } @@ -116,14 +113,6 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt private void TerritoryClear( object? _1, ushort _2 ) => _dirty = _cache.Count > 0; - private delegate void CharacterDestructorDelegate( Character* character ); - - [Signature( Sigs.CharacterDestructor, DetourName = nameof( CharacterDestructorDetour ) )] - private Hook< CharacterDestructorDelegate > _characterDtorHook = null!; - - private void CharacterDestructorDetour( Character* character ) - { - _cache.Remove( ( IntPtr )character ); - _characterDtorHook.Original( character ); - } + private void OnCharacterDestruct( Character* character ) + => _cache.Remove( ( IntPtr )character ); } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs index a705fd6d..016ca38d 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Subfiles.cs @@ -24,18 +24,20 @@ public unsafe partial class PathResolver // Thus, we need to ensure the correct files are loaded when a material is loaded. public class SubfileHelper : IDisposable, IReadOnlyCollection< KeyValuePair< IntPtr, ResolveData > > { - private readonly ResourceLoader _loader; + private readonly ResourceLoader _loader; + private readonly GameEventManager _events; private readonly ThreadLocal< ResolveData > _mtrlData = new(() => ResolveData.Invalid); private readonly ThreadLocal< ResolveData > _avfxData = new(() => ResolveData.Invalid); private readonly ConcurrentDictionary< IntPtr, ResolveData > _subFileCollection = new(); - public SubfileHelper( ResourceLoader loader ) + public SubfileHelper( ResourceLoader loader, GameEventManager events ) { SignatureHelper.Initialise( this ); _loader = loader; + _events = events; } // Check specifically for shpk and tex files whether we are currently in a material load. @@ -85,7 +87,7 @@ public unsafe partial class PathResolver _apricotResourceLoadHook.Enable(); _loader.ResourceLoadCustomization += SubfileLoadHandler; _loader.ResourceLoaded += SubfileContainerRequested; - _loader.FileLoaded += SubfileContainerLoaded; + _events.ResourceHandleDestructor += ResourceDestroyed; } public void Disable() @@ -95,7 +97,7 @@ public unsafe partial class PathResolver _apricotResourceLoadHook.Disable(); _loader.ResourceLoadCustomization -= SubfileLoadHandler; _loader.ResourceLoaded -= SubfileContainerRequested; - _loader.FileLoaded -= SubfileContainerLoaded; + _events.ResourceHandleDestructor -= ResourceDestroyed; } public void Dispose() @@ -121,16 +123,8 @@ public unsafe partial class PathResolver } } - private void SubfileContainerLoaded( ResourceHandle* handle, ByteString path, bool success, bool custom ) - { - switch( handle->FileType ) - { - case ResourceType.Mtrl: - case ResourceType.Avfx: - _subFileCollection.TryRemove( ( IntPtr )handle, out _ ); - break; - } - } + private void ResourceDestroyed( ResourceHandle* handle ) + => _subFileCollection.TryRemove( ( IntPtr )handle, out _ ); // We need to set the correct collection for the actual material path that is loaded // before actually loading the file. diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 2ece65f6..e00e9c45 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -24,10 +24,10 @@ public partial class PathResolver : IDisposable public bool Enabled { get; private set; } private readonly ResourceLoader _loader; - private static readonly CutsceneCharacters Cutscenes = new(); + private static readonly CutsceneCharacters Cutscenes = new(Penumbra.GameEvents); private static readonly DrawObjectState DrawObjects = new(); private static readonly BitArray ValidHumanModels; - internal static readonly IdentifiedCollectionCache IdentifiedCache = new(); + internal static readonly IdentifiedCollectionCache IdentifiedCache = new(Penumbra.GameEvents); private readonly AnimationState _animations; private readonly PathState _paths; private readonly MetaState _meta; @@ -43,7 +43,7 @@ public partial class PathResolver : IDisposable _animations = new AnimationState( DrawObjects ); _paths = new PathState( this ); _meta = new MetaState( _paths.HumanVTable ); - _subFiles = new SubfileHelper( _loader ); + _subFiles = new SubfileHelper( _loader, Penumbra.GameEvents ); } // The modified resolver that handles game path resolving. @@ -175,6 +175,9 @@ public partial class PathResolver : IDisposable internal IEnumerable< KeyValuePair< IntPtr, ResolveData > > ResourceCollections => _subFiles; + internal int SubfileCount + => _subFiles.Count; + internal ResolveData CurrentMtrlData => _subFiles.MtrlData; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 2c90c01d..9d4d31e1 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -54,6 +54,7 @@ public class Penumbra : IDalamudPlugin public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; + public static GameEventManager GameEvents { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!; public static Mod.Manager ModManager { get; private set; } = null!; public static ModCollection.Manager CollectionManager { get; private set; } = null!; @@ -88,11 +89,12 @@ public class Penumbra : IDalamudPlugin try { Dalamud.Initialize( pluginInterface ); - Performance = new PerformanceTracker< PerformanceType >( Dalamud.Framework ); + Performance = new PerformanceTracker< PerformanceType >( Dalamud.Framework ); Log = new Logger(); DevPenumbraExists = CheckDevPluginPenumbra(); IsNotInstalledPenumbra = CheckIsNotInstalled(); IsValidSourceRepo = CheckSourceRepo(); + GameEvents = new GameEventManager(); Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ); GamePathParser = GameData.GameData.GetGamePathParser(); StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ); @@ -305,6 +307,7 @@ public class Penumbra : IDalamudPlugin PathResolver?.Dispose(); ResourceLogger?.Dispose(); ResourceLoader?.Dispose(); + GameEvents?.Dispose(); CharacterUtility?.Dispose(); Performance?.Dispose(); }