diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index c5066174..48c8a2e2 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -162,10 +162,15 @@ public partial class ActorManager ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID); } case ObjectKind.EventNpc: + { + var dataId = actor->DataID; + // Special case for squadron that is also in the game functions, cf. E8 ?? ?? ?? ?? 89 87 ?? ?? ?? ?? 4C 89 BF + if (dataId == 0xf845d) + dataId = actor->GetNpcID(); return check - ? CreateNpc(ObjectKind.EventNpc, actor->DataID, actor->ObjectIndex) - : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, actor->ObjectIndex, ObjectKind.EventNpc, - actor->ObjectIndex); + ? CreateNpc(ObjectKind.EventNpc, dataId, actor->ObjectIndex) + : CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, actor->ObjectIndex, ObjectKind.EventNpc, dataId); + } case ObjectKind.MountType: case ObjectKind.Companion: case (ObjectKind)15: // TODO: CS Update diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index bb41f6f1..132b772a 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -35,6 +35,8 @@ public class TempModManager public bool CollectionByName( string name, [NotNullWhen( true )] out ModCollection? collection ) => Collections.Values.FindFirst( c => string.Equals( c.Name, name, StringComparison.OrdinalIgnoreCase ), out collection ); + public event ModCollection.Manager.CollectionChangeDelegate? CollectionChanged; + // These functions to check specific redirections or meta manipulations for existence are currently unused. //public bool IsRegistered( string tag, ModCollection? collection, Utf8GamePath gamePath, out FullPath? fullPath, out int priority ) //{ @@ -151,6 +153,7 @@ public class TempModManager { var collection = ModCollection.CreateNewTemporary( tag, characterName ); _collections[ characterName ] = collection; + CollectionChanged?.Invoke(CollectionType.Temporary, null, collection ); return collection.Name; } @@ -160,6 +163,7 @@ public class TempModManager { _mods.Remove( c ); c.ClearCache(); + CollectionChanged?.Invoke( CollectionType.Temporary, c, null ); return true; } diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index e30dcece..a8feb7f0 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -126,7 +126,7 @@ public partial class ModCollection RemoveCache( oldCollectionIdx ); UpdateCurrentCollectionInUse(); - CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, Individuals[ individualIndex ].DisplayName ); + CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, collectionType == CollectionType.Individual ? Individuals[ individualIndex ].DisplayName : string.Empty ); } private void UpdateCurrentCollectionInUse() diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index fed83db5..30da217d 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -99,6 +99,7 @@ public enum CollectionType : byte Interface, // The ui collection was changed Individual, // An individual collection was changed Current, // The current collection was changed + Temporary, // A temporary collections was set or deleted via IPC } public static class CollectionTypeExtensions diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs new file mode 100644 index 00000000..a6692839 --- /dev/null +++ b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs @@ -0,0 +1,129 @@ +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.Actors; + +namespace Penumbra.Interop.Resolver; + +public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPtr Address, ActorIdentifier Identifier, ModCollection Collection) > +{ + private readonly Dictionary< IntPtr, (ActorIdentifier, ModCollection) > _cache = new(317); + private bool _dirty = false; + private bool _enabled = false; + + public IdentifiedCollectionCache() + { + SignatureHelper.Initialise( this ); + } + + public void Enable() + { + if( _enabled ) + { + return; + } + + Penumbra.CollectionManager.CollectionChanged += CollectionChangeClear; + Penumbra.TempMods.CollectionChanged += CollectionChangeClear; + Dalamud.ClientState.TerritoryChanged += TerritoryClear; + _characterDtorHook.Enable(); + _enabled = true; + } + + public void Disable() + { + if( !_enabled ) + { + return; + } + + Penumbra.CollectionManager.CollectionChanged -= CollectionChangeClear; + Penumbra.TempMods.CollectionChanged -= CollectionChangeClear; + Dalamud.ClientState.TerritoryChanged -= TerritoryClear; + _characterDtorHook.Disable(); + _enabled = false; + } + + public ResolveData Set( ModCollection collection, ActorIdentifier identifier, GameObject* data ) + { + if( _dirty ) + { + _dirty = false; + _cache.Clear(); + } + + _cache[ ( IntPtr )data ] = ( identifier, collection ); + return collection.ToResolveData( data ); + } + + public bool TryGetValue( GameObject* gameObject, out ResolveData resolve ) + { + if( _dirty ) + { + _dirty = false; + _cache.Clear(); + } + else if( _cache.TryGetValue( ( IntPtr )gameObject, out var p ) ) + { + resolve = p.Item2.ToResolveData( gameObject ); + return true; + } + + resolve = default; + return false; + } + + public void Dispose() + { + Disable(); + _characterDtorHook.Dispose(); + GC.SuppressFinalize( this ); + } + + public IEnumerator< (IntPtr Address, ActorIdentifier Identifier, ModCollection Collection) > GetEnumerator() + { + foreach( var (address, (identifier, collection)) in _cache ) + { + if( _dirty ) + { + yield break; + } + + yield return ( address, identifier, collection ); + } + } + + ~IdentifiedCollectionCache() + => Dispose(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + private void CollectionChangeClear( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 ) + { + if( type is not (CollectionType.Current or CollectionType.Interface or CollectionType.Inactive) ) + { + _dirty = _cache.Count > 0; + } + } + + private void TerritoryClear( object? _1, ushort _2 ) + => _dirty = _cache.Count > 0; + + private delegate void CharacterDestructorDelegate( Character* character ); + + [Signature( "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", + DetourName = nameof( CharacterDestructorDetour ) )] + private Hook< CharacterDestructorDelegate > _characterDtorHook = null!; + + private void CharacterDestructorDetour( Character* character ) + { + _cache.Remove( ( IntPtr )character ); + _characterDtorHook.Original( character ); + } +} \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 3744bb67..3c842250 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -118,7 +118,7 @@ public unsafe partial class PathResolver if( idx >= 0 && idx < Dalamud.Objects.Length ) { var obj = Dalamud.Objects[ idx ]; - _animationLoadData = obj != null ? IdentifyCollection( ( GameObject* )obj.Address ) : ResolveData.Invalid; + _animationLoadData = obj != null ? IdentifyCollection( ( GameObject* )obj.Address, true ) : ResolveData.Invalid; } else { @@ -165,7 +165,7 @@ public unsafe partial class PathResolver private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 ) { var last = _animationLoadData; - _animationLoadData = IdentifyCollection( ( GameObject* )gameObject ); + _animationLoadData = IdentifyCollection( ( GameObject* )gameObject, true ); var ret = _loadSomeAvfxHook.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 ); _animationLoadData = last; return ret; @@ -187,7 +187,7 @@ public unsafe partial class PathResolver var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 ); if( actorIdx >= 0 && actorIdx < Dalamud.Objects.Length ) { - _animationLoadData = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) ); + _animationLoadData = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ), true ); } } @@ -202,7 +202,7 @@ public unsafe partial class PathResolver private void SomeActionLoadDetour( IntPtr gameObject ) { var last = _animationLoadData; - _animationLoadData = IdentifyCollection( ( GameObject* )gameObject ); + _animationLoadData = IdentifyCollection( ( GameObject* )gameObject, true ); _someActionLoadHook.Original( gameObject ); _animationLoadData = last; } @@ -214,7 +214,7 @@ public unsafe partial class PathResolver { var last = _animationLoadData; var gameObject = ( GameObject* )( unk - 0x8D0 ); - _animationLoadData = IdentifyCollection( gameObject ); + _animationLoadData = IdentifyCollection( gameObject, true ); _someOtherAvfxHook.Original( unk ); _animationLoadData = last; } diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index bf335f55..47cd5e6d 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -44,7 +44,7 @@ public unsafe partial class PathResolver { if( parentObject == IntPtr.Zero && LastGameObject != null ) { - var collection = IdentifyCollection( LastGameObject ); + var collection = IdentifyCollection( LastGameObject, true ); _drawObjectToObject[ drawObject ] = ( collection, LastGameObject->ObjectIndex ); return collection; } @@ -86,6 +86,7 @@ public unsafe partial class PathResolver _weaponReloadHook.Enable(); InitializeDrawObjects(); Penumbra.CollectionManager.CollectionChanged += CheckCollections; + Penumbra.TempMods.CollectionChanged += CheckCollections; } public void Disable() @@ -95,6 +96,7 @@ public unsafe partial class PathResolver _enableDrawHook.Disable(); _weaponReloadHook.Disable(); Penumbra.CollectionManager.CollectionChanged -= CheckCollections; + Penumbra.TempMods.CollectionChanged -= CheckCollections; } public void Dispose() @@ -139,7 +141,7 @@ public unsafe partial class PathResolver var meta = DisposableContainer.Empty; if( LastGameObject != null ) { - _lastCreatedCollection = IdentifyCollection( LastGameObject ); + _lastCreatedCollection = IdentifyCollection( LastGameObject, false ); // Change the transparent or 1.0 Decal if necessary. var decal = new CharacterUtility.DecalReverter( _lastCreatedCollection.ModCollection, UsesDecal( a, c ) ); // Change the rsp parameters if necessary. @@ -235,7 +237,7 @@ public unsafe partial class PathResolver _drawObjectToObject.Remove( key ); } - var newCollection = IdentifyCollection( obj ); + var newCollection = IdentifyCollection( obj, false ); _drawObjectToObject[ key ] = ( newCollection, idx ); } } @@ -249,7 +251,7 @@ public unsafe partial class PathResolver var ptr = ( GameObject* )Dalamud.Objects.GetObjectAddress( i ); if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null ) { - _drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr ), ptr->ObjectIndex ); + _drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr, false ), ptr->ObjectIndex ); } } } diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 18c55ca8..d031b153 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -17,7 +17,7 @@ namespace Penumbra.Interop.Resolver; public unsafe partial class PathResolver { // Identify the correct collection for a GameObject by index and name. - private static ResolveData IdentifyCollection( GameObject* gameObject ) + private static ResolveData IdentifyCollection( GameObject* gameObject, bool useCache ) { if( gameObject == null ) { @@ -26,6 +26,11 @@ public unsafe partial class PathResolver try { + if( useCache && IdentifiedCache.TryGetValue( gameObject, out var data ) ) + { + return data; + } + // Login screen. Names are populated after actors are drawn, // so it is not possible to fetch names from the ui list. // Actors are also not named. So use Yourself > Players > Racial > Default. @@ -34,7 +39,7 @@ public unsafe partial class PathResolver var collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) ?? CollectionByAttributes( gameObject ) ?? Penumbra.CollectionManager.Default; - return collection.ToResolveData( gameObject ); + return IdentifiedCache.Set( collection, ActorIdentifier.Invalid, gameObject ); } else { @@ -44,7 +49,7 @@ public unsafe partial class PathResolver ?? CollectionByAttributes( gameObject ) ?? CheckOwnedCollection( identifier, gameObject ) ?? Penumbra.CollectionManager.Default; - return collection.ToResolveData( gameObject ); + return IdentifiedCache.Set( collection, identifier, gameObject ); } } catch( Exception e ) diff --git a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs index 0e0716ea..e3fb47e0 100644 --- a/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs +++ b/Penumbra/Interop/Resolver/PathResolver.ResolverHooks.cs @@ -3,7 +3,6 @@ using System.Runtime.CompilerServices; using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; -using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 4f9b5059..aa1f9055 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -26,14 +26,15 @@ public partial class PathResolver : IDisposable { public bool Enabled { get; private set; } - private readonly ResourceLoader _loader; - private static readonly CutsceneCharacters Cutscenes = new(); - private static readonly DrawObjectState DrawObjects = new(); - private static readonly BitArray ValidHumanModels; - private readonly AnimationState _animations; - private readonly PathState _paths; - private readonly MetaState _meta; - private readonly MaterialState _materials; + private readonly ResourceLoader _loader; + private static readonly CutsceneCharacters Cutscenes = new(); + private static readonly DrawObjectState DrawObjects = new(); + private static readonly BitArray ValidHumanModels; + internal static readonly IdentifiedCollectionCache IdentifiedCache = new(); + private readonly AnimationState _animations; + private readonly PathState _paths; + private readonly MetaState _meta; + private readonly MaterialState _materials; static PathResolver() => ValidHumanModels = GetValidHumanModels( Dalamud.GameData ); @@ -87,6 +88,7 @@ public partial class PathResolver : IDisposable Enabled = true; Cutscenes.Enable(); DrawObjects.Enable(); + IdentifiedCache.Enable(); _animations.Enable(); _paths.Enable(); _meta.Enable(); @@ -107,6 +109,7 @@ public partial class PathResolver : IDisposable _animations.Disable(); DrawObjects.Disable(); Cutscenes.Disable(); + IdentifiedCache.Disable(); _paths.Disable(); _meta.Disable(); _materials.Disable(); @@ -122,6 +125,7 @@ public partial class PathResolver : IDisposable _animations.Dispose(); DrawObjects.Dispose(); Cutscenes.Dispose(); + IdentifiedCache.Dispose(); _meta.Dispose(); _materials.Dispose(); } @@ -147,11 +151,11 @@ public partial class PathResolver : IDisposable if( DrawObjects.LastGameObject != null && ( DrawObjects.LastGameObject->DrawObject == null || DrawObjects.LastGameObject->DrawObject == ( DrawObject* )drawObject ) ) { - resolveData = IdentifyCollection( DrawObjects.LastGameObject ); + resolveData = IdentifyCollection( DrawObjects.LastGameObject, true ); return DrawObjects.LastGameObject; } - resolveData = IdentifyCollection( null ); + resolveData = IdentifyCollection( null, true ); return null; } diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index ac8beade..7681d09c 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -14,7 +14,6 @@ using System.IO; using System.Linq; using System.Numerics; using Penumbra.Api.Enums; -using Penumbra.GameData.Actors; namespace Penumbra.UI.Classes; diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 4142faa5..57351b71 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -12,6 +12,7 @@ using OtterGui.Raii; using Penumbra.GameData.Actors; using Penumbra.GameData.Files; using Penumbra.Interop.Loader; +using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using Penumbra.String; using CharacterUtility = Penumbra.Interop.CharacterUtility; @@ -250,6 +251,23 @@ public partial class ConfigWindow } } + using( var identifiedTree = ImRaii.TreeNode( "Identified Collections" ) ) + { + if( identifiedTree ) + { + using var table = ImRaii.Table( "##PathCollectionsIdentifiedTable", 3, ImGuiTableFlags.SizingFixedFit ); + if( table ) + { + foreach( var (address, identifier, collection) in PathResolver.IdentifiedCache ) + { + ImGuiUtil.DrawTableColumn( $"0x{address:X}" ); + ImGuiUtil.DrawTableColumn( identifier.ToString() ); + ImGuiUtil.DrawTableColumn( collection.Name ); + } + } + } + } + using var cutsceneTree = ImRaii.TreeNode( "Cutscene Actors" ); if( cutsceneTree ) {