diff --git a/OtterGui b/OtterGui index f6a8ad0f..22ae2a89 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f6a8ad0f8e585408e0aa17c90209358403b52535 +Subproject commit 22ae2a8993ebf3af2313072968a44905a3fcdd2a diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index b7a46ae2..2a7a9bfb 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -259,7 +259,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } else if (tab != TabType.None) { - _communicator.SelectTab.Invoke(tab); + _communicator.SelectTab.Invoke(tab, null); } return PenumbraApiEc.Success; diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index ea389bb6..b11f2306 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -11,7 +11,7 @@ namespace Penumbra.Communication; /// Parameter is the clicked object data if any. /// /// -public sealed class ChangedItemClick : EventWrapper, ChangedItemClick.Priority> +public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) { public enum Priority { @@ -21,11 +21,4 @@ public sealed class ChangedItemClick : EventWrapper /// Link = 1, } - - public ChangedItemClick() - : base(nameof(ChangedItemClick)) - { } - - public void Invoke(MouseButton button, object? data) - => Invoke(this, button, data); } diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index cf270ba0..10607da4 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -8,7 +8,7 @@ namespace Penumbra.Communication; /// Parameter is the hovered object data if any. /// /// -public sealed class ChangedItemHover : EventWrapper, ChangedItemHover.Priority> +public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) { public enum Priority { @@ -19,13 +19,6 @@ public sealed class ChangedItemHover : EventWrapper, ChangedItem Link = 1, } - public ChangedItemHover() - : base(nameof(ChangedItemHover)) - { } - - public void Invoke(object? data) - => Invoke(this, data); - public bool HasTooltip => HasSubscribers; } diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs index b713cc72..95d4ac4d 100644 --- a/Penumbra/Communication/CollectionChange.cs +++ b/Penumbra/Communication/CollectionChange.cs @@ -12,7 +12,8 @@ namespace Penumbra.Communication; /// Parameter is the new collection, or null on deletions. /// Parameter is the display name for Individual collections or an empty string otherwise. /// -public sealed class CollectionChange : EventWrapper, CollectionChange.Priority> +public sealed class CollectionChange() + : EventWrapper(nameof(CollectionChange)) { public enum Priority { @@ -46,11 +47,4 @@ public sealed class CollectionChange : EventWrapper ModFileSystemSelector = 0, } - - public CollectionChange() - : base(nameof(CollectionChange)) - { } - - public void Invoke(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string displayName) - => Invoke(this, collectionType, oldCollection, newCollection, displayName); } diff --git a/Penumbra/Communication/CollectionInheritanceChanged.cs b/Penumbra/Communication/CollectionInheritanceChanged.cs index 8288341d..dbcf9e4a 100644 --- a/Penumbra/Communication/CollectionInheritanceChanged.cs +++ b/Penumbra/Communication/CollectionInheritanceChanged.cs @@ -10,7 +10,8 @@ namespace Penumbra.Communication; /// Parameter is whether the change was itself inherited, i.e. if it happened in a direct parent (false) or a more removed ancestor (true). /// /// -public sealed class CollectionInheritanceChanged : EventWrapper, CollectionInheritanceChanged.Priority> +public sealed class CollectionInheritanceChanged() + : EventWrapper(nameof(CollectionInheritanceChanged)) { public enum Priority { @@ -23,11 +24,4 @@ public sealed class CollectionInheritanceChanged : EventWrapper ModFileSystemSelector = 0, } - - public CollectionInheritanceChanged() - : base(nameof(CollectionInheritanceChanged)) - { } - - public void Invoke(ModCollection collection, bool inherited) - => Invoke(this, collection, inherited); } diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs index b1903e5b..397f7bfd 100644 --- a/Penumbra/Communication/CreatedCharacterBase.cs +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -9,18 +9,12 @@ namespace Penumbra.Communication; /// Parameter is the applied collection. /// Parameter is the created draw object. /// -public sealed class CreatedCharacterBase : EventWrapper, CreatedCharacterBase.Priority> +public sealed class CreatedCharacterBase() + : EventWrapper(nameof(CreatedCharacterBase)) { public enum Priority { /// Api = int.MinValue, } - - public CreatedCharacterBase() - : base(nameof(CreatedCharacterBase)) - { } - - public void Invoke(nint gameObject, ModCollection appliedCollection, nint drawObject) - => Invoke(this, gameObject, appliedCollection, drawObject); } diff --git a/Penumbra/Communication/CreatingCharacterBase.cs b/Penumbra/Communication/CreatingCharacterBase.cs index 090ca40b..1e232761 100644 --- a/Penumbra/Communication/CreatingCharacterBase.cs +++ b/Penumbra/Communication/CreatingCharacterBase.cs @@ -12,18 +12,12 @@ namespace Penumbra.Communication; /// Parameter is a pointer to the customize array. /// Parameter is a pointer to the equip data array. /// -public sealed class CreatingCharacterBase : EventWrapper, CreatingCharacterBase.Priority> +public sealed class CreatingCharacterBase() + : EventWrapper(nameof(CreatingCharacterBase)) { public enum Priority { /// Api = 0, } - - public CreatingCharacterBase() - : base(nameof(CreatingCharacterBase)) - { } - - public void Invoke(nint gameObject, string appliedCollectionName, nint modelIdAddress, nint customizeArrayAddress, nint equipDataAddress) - => Invoke(this, gameObject, appliedCollectionName, modelIdAddress, customizeArrayAddress, equipDataAddress); } diff --git a/Penumbra/Communication/EnabledChanged.cs b/Penumbra/Communication/EnabledChanged.cs index fa768235..be6343b7 100644 --- a/Penumbra/Communication/EnabledChanged.cs +++ b/Penumbra/Communication/EnabledChanged.cs @@ -9,7 +9,7 @@ namespace Penumbra.Communication; /// Parameter is whether Penumbra is now Enabled (true) or Disabled (false). /// /// -public sealed class EnabledChanged : EventWrapper, EnabledChanged.Priority> +public sealed class EnabledChanged() : EventWrapper(nameof(EnabledChanged)) { public enum Priority { @@ -19,11 +19,4 @@ public sealed class EnabledChanged : EventWrapper, EnabledChanged.P /// DalamudSubstitutionProvider = 0, } - - public EnabledChanged() - : base(nameof(EnabledChanged)) - { } - - public void Invoke(bool enabled) - => Invoke(this, enabled); } diff --git a/Penumbra/Communication/ModDataChanged.cs b/Penumbra/Communication/ModDataChanged.cs index 2f50f005..ffa43d43 100644 --- a/Penumbra/Communication/ModDataChanged.cs +++ b/Penumbra/Communication/ModDataChanged.cs @@ -11,7 +11,7 @@ namespace Penumbra.Communication; /// Parameter is the changed mod. /// Parameter is the old name of the mod in case of a name change, and null otherwise. /// -public sealed class ModDataChanged : EventWrapper, ModDataChanged.Priority> +public sealed class ModDataChanged() : EventWrapper(nameof(ModDataChanged)) { public enum Priority { @@ -27,11 +27,4 @@ public sealed class ModDataChanged : EventWrapper ModPanelHeader = 0, } - - public ModDataChanged() - : base(nameof(ModDataChanged)) - { } - - public void Invoke(ModDataChangeType changeType, Mod mod, string? oldName) - => Invoke(this, changeType, mod, oldName); } diff --git a/Penumbra/Communication/ModDirectoryChanged.cs b/Penumbra/Communication/ModDirectoryChanged.cs index 9fdb261e..20d13b20 100644 --- a/Penumbra/Communication/ModDirectoryChanged.cs +++ b/Penumbra/Communication/ModDirectoryChanged.cs @@ -10,7 +10,7 @@ namespace Penumbra.Communication; /// Parameter is whether the new directory is valid. /// /// -public sealed class ModDirectoryChanged : EventWrapper, ModDirectoryChanged.Priority> +public sealed class ModDirectoryChanged() : EventWrapper(nameof(ModDirectoryChanged)) { public enum Priority { @@ -20,11 +20,4 @@ public sealed class ModDirectoryChanged : EventWrapper, Mod /// FileDialogService = 0, } - - public ModDirectoryChanged() - : base(nameof(ModDirectoryChanged)) - { } - - public void Invoke(string newModDirectory, bool newDirectoryValid) - => Invoke(this, newModDirectory, newDirectoryValid); } diff --git a/Penumbra/Communication/ModDiscoveryFinished.cs b/Penumbra/Communication/ModDiscoveryFinished.cs index 04c13e95..759ea42e 100644 --- a/Penumbra/Communication/ModDiscoveryFinished.cs +++ b/Penumbra/Communication/ModDiscoveryFinished.cs @@ -1,10 +1,9 @@ using OtterGui.Classes; -using Penumbra.Mods.Manager; namespace Penumbra.Communication; /// Triggered whenever a new mod discovery has finished. -public sealed class ModDiscoveryFinished : EventWrapper +public sealed class ModDiscoveryFinished() : EventWrapper(nameof(ModDiscoveryFinished)) { public enum Priority { @@ -23,11 +22,4 @@ public sealed class ModDiscoveryFinished : EventWrapper ModFileSystem = 0, } - - public ModDiscoveryFinished() - : base(nameof(ModDiscoveryFinished)) - { } - - public void Invoke() - => Invoke(this); } diff --git a/Penumbra/Communication/ModDiscoveryStarted.cs b/Penumbra/Communication/ModDiscoveryStarted.cs index cf45528d..5cafd1ea 100644 --- a/Penumbra/Communication/ModDiscoveryStarted.cs +++ b/Penumbra/Communication/ModDiscoveryStarted.cs @@ -3,7 +3,7 @@ using OtterGui.Classes; namespace Penumbra.Communication; /// Triggered whenever mods are prepared to be rediscovered. -public sealed class ModDiscoveryStarted : EventWrapper +public sealed class ModDiscoveryStarted() : EventWrapper(nameof(ModDiscoveryStarted)) { public enum Priority { @@ -16,11 +16,4 @@ public sealed class ModDiscoveryStarted : EventWrapper ModFileSystemSelector = 200, } - - public ModDiscoveryStarted() - : base(nameof(ModDiscoveryStarted)) - { } - - public void Invoke() - => Invoke(this); } diff --git a/Penumbra/Communication/ModOptionChanged.cs b/Penumbra/Communication/ModOptionChanged.cs index 416cc8df..a0b4d26c 100644 --- a/Penumbra/Communication/ModOptionChanged.cs +++ b/Penumbra/Communication/ModOptionChanged.cs @@ -13,7 +13,8 @@ namespace Penumbra.Communication; /// Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. /// Parameter is the index of the group an option was moved to. /// -public sealed class ModOptionChanged : EventWrapper, ModOptionChanged.Priority> +public sealed class ModOptionChanged() + : EventWrapper(nameof(ModOptionChanged)) { public enum Priority { @@ -29,11 +30,4 @@ public sealed class ModOptionChanged : EventWrapper CollectionStorage = 100, } - - public ModOptionChanged() - : base(nameof(ModOptionChanged)) - { } - - public void Invoke(ModOptionChangeType changeType, Mod mod, int groupIndex, int optionIndex, int moveToIndex) - => Invoke(this, changeType, mod, groupIndex, optionIndex, moveToIndex); } diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index 83c3b5a5..3ec64f7e 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -14,7 +14,8 @@ namespace Penumbra.Communication; /// Parameter is the new directory on addition, move or reload and null on deletion. /// /// -public sealed class ModPathChanged : EventWrapper, ModPathChanged.Priority> +public sealed class ModPathChanged() + : EventWrapper(nameof(ModPathChanged)) { public enum Priority { @@ -48,11 +49,4 @@ public sealed class ModPathChanged : EventWrapper CollectionCacheManagerRemoval = 100, } - - public ModPathChanged() - : base(nameof(ModPathChanged)) - { } - - public void Invoke(ModPathChangeType changeType, Mod mod, DirectoryInfo? oldModDirectory, DirectoryInfo? newModDirectory) - => Invoke(this, changeType, mod, oldModDirectory, newModDirectory); } diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index 65bcf3ed..5e0bc0c0 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -17,7 +17,8 @@ namespace Penumbra.Communication; /// Parameter is whether the change was inherited from another collection. /// /// -public sealed class ModSettingChanged : EventWrapper, ModSettingChanged.Priority> +public sealed class ModSettingChanged() + : EventWrapper(nameof(ModSettingChanged)) { public enum Priority { @@ -33,11 +34,4 @@ public sealed class ModSettingChanged : EventWrapper ModFileSystemSelector = 0, } - - public ModSettingChanged() - : base(nameof(ModSettingChanged)) - { } - - public void Invoke(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool inherited) - => Invoke(this, collection, type, mod, oldValue, groupIdx, inherited); } diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlShpkLoaded.cs index 868692cd..bd560fd8 100644 --- a/Penumbra/Communication/MtrlShpkLoaded.cs +++ b/Penumbra/Communication/MtrlShpkLoaded.cs @@ -6,18 +6,11 @@ namespace Penumbra.Communication; /// Parameter is the material resource handle for which the shader package has been loaded. /// Parameter is the associated game object. /// -public sealed class MtrlShpkLoaded : EventWrapper, MtrlShpkLoaded.Priority> +public sealed class MtrlShpkLoaded() : EventWrapper(nameof(MtrlShpkLoaded)) { public enum Priority { /// SkinFixer = 0, } - - public MtrlShpkLoaded() - : base(nameof(MtrlShpkLoaded)) - { } - - public void Invoke(nint mtrlResourceHandle, nint gameObject) - => Invoke(this, mtrlResourceHandle, gameObject); } diff --git a/Penumbra/Communication/PostSettingsPanelDraw.cs b/Penumbra/Communication/PostSettingsPanelDraw.cs index b32b0dfa..a918b610 100644 --- a/Penumbra/Communication/PostSettingsPanelDraw.cs +++ b/Penumbra/Communication/PostSettingsPanelDraw.cs @@ -8,18 +8,11 @@ namespace Penumbra.Communication; /// Parameter is the identifier (directory name) of the currently selected mod. /// /// -public sealed class PostSettingsPanelDraw : EventWrapper, PostSettingsPanelDraw.Priority> +public sealed class PostSettingsPanelDraw() : EventWrapper(nameof(PostSettingsPanelDraw)) { public enum Priority { /// Default = 0, } - - public PostSettingsPanelDraw() - : base(nameof(PostSettingsPanelDraw)) - { } - - public void Invoke(string modDirectory) - => Invoke(this, modDirectory); } diff --git a/Penumbra/Communication/PreSettingsPanelDraw.cs b/Penumbra/Communication/PreSettingsPanelDraw.cs index e5857474..cda00d78 100644 --- a/Penumbra/Communication/PreSettingsPanelDraw.cs +++ b/Penumbra/Communication/PreSettingsPanelDraw.cs @@ -8,18 +8,11 @@ namespace Penumbra.Communication; /// Parameter is the identifier (directory name) of the currently selected mod. /// /// -public sealed class PreSettingsPanelDraw : EventWrapper, PreSettingsPanelDraw.Priority> +public sealed class PreSettingsPanelDraw() : EventWrapper(nameof(PreSettingsPanelDraw)) { public enum Priority { /// Default = 0, } - - public PreSettingsPanelDraw() - : base(nameof(PreSettingsPanelDraw)) - { } - - public void Invoke(string modDirectory) - => Invoke(this, modDirectory); } diff --git a/Penumbra/Communication/ResolvedFileChanged.cs b/Penumbra/Communication/ResolvedFileChanged.cs index 55e95320..3211a26a 100644 --- a/Penumbra/Communication/ResolvedFileChanged.cs +++ b/Penumbra/Communication/ResolvedFileChanged.cs @@ -15,8 +15,9 @@ namespace Penumbra.Communication; /// Parameter is the old redirection path for Replaced, or empty. /// Parameter is the mod responsible for the new redirection if any. /// -public sealed class ResolvedFileChanged : EventWrapper, - ResolvedFileChanged.Priority> +public sealed class ResolvedFileChanged() + : EventWrapper( + nameof(ResolvedFileChanged)) { public enum Type { @@ -29,14 +30,7 @@ public sealed class ResolvedFileChanged : EventWrapper + /// DalamudSubstitutionProvider = 0, } - - public ResolvedFileChanged() - : base(nameof(ResolvedFileChanged)) - { } - - public void Invoke(ModCollection collection, Type type, Utf8GamePath key, FullPath value, FullPath old, IMod? mod) - => Invoke(this, collection, type, key, value, old, mod); } diff --git a/Penumbra/Communication/SelectTab.cs b/Penumbra/Communication/SelectTab.cs index aaa362f6..cb7e2e56 100644 --- a/Penumbra/Communication/SelectTab.cs +++ b/Penumbra/Communication/SelectTab.cs @@ -11,18 +11,11 @@ namespace Penumbra.Communication; /// Parameter is the selected mod, if any. /// /// -public sealed class SelectTab : EventWrapper, SelectTab.Priority> +public sealed class SelectTab() : EventWrapper(nameof(SelectTab)) { public enum Priority { /// ConfigTabBar = 0, } - - public SelectTab() - : base(nameof(SelectTab)) - { } - - public void Invoke(TabType tab = TabType.None, Mod? mod = null) - => Invoke(this, tab, mod); } diff --git a/Penumbra/Communication/TemporaryGlobalModChange.cs b/Penumbra/Communication/TemporaryGlobalModChange.cs index 12d42e48..6edf26d7 100644 --- a/Penumbra/Communication/TemporaryGlobalModChange.cs +++ b/Penumbra/Communication/TemporaryGlobalModChange.cs @@ -10,7 +10,8 @@ namespace Penumbra.Communication; /// Parameter is whether the mod was newly created. /// Parameter is whether the mod was deleted. /// -public sealed class TemporaryGlobalModChange : EventWrapper, TemporaryGlobalModChange.Priority> +public sealed class TemporaryGlobalModChange() + : EventWrapper(nameof(TemporaryGlobalModChange)) { public enum Priority { @@ -20,11 +21,4 @@ public sealed class TemporaryGlobalModChange : EventWrapper TempCollectionManager = 0, } - - public TemporaryGlobalModChange() - : base(nameof(TemporaryGlobalModChange)) - { } - - public void Invoke(TemporaryMod temporaryMod, bool newlyCreated, bool deleted) - => Invoke(this, temporaryMod, newlyCreated, deleted); } diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs new file mode 100644 index 00000000..2552f1a7 --- /dev/null +++ b/Penumbra/Interop/GameState.cs @@ -0,0 +1,117 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Interop.PathResolving; +using Penumbra.String.Classes; + +namespace Penumbra.Interop; + +public class GameState : IService +{ + #region Last Game Object + + private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); + + public nint LastGameObject + => _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public unsafe void QueueGameObject(GameObject* gameObject) + => QueueGameObject((nint)gameObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void QueueGameObject(nint gameObject) + => _lastGameObject.Value!.Enqueue(gameObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void DequeueGameObject() + => _lastGameObject.Value!.TryDequeue(out _); + + #endregion + + #region Animation Data + + private readonly ThreadLocal _animationLoadData = new(() => ResolveData.Invalid, true); + + public ResolveData AnimationData + => _animationLoadData.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public ResolveData SetAnimationData(ResolveData data) + { + var old = _animationLoadData.Value; + _animationLoadData.Value = data; + return old; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void RestoreAnimationData(ResolveData old) + => _animationLoadData.Value = old; + + #endregion + + #region Sound Data + + private readonly ThreadLocal _characterSoundData = new(() => ResolveData.Invalid, true); + + public ResolveData SoundData + => _animationLoadData.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public ResolveData SetSoundData(ResolveData data) + { + var old = _characterSoundData.Value; + _characterSoundData.Value = data; + return old; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void RestoreSoundData(ResolveData old) + => _characterSoundData.Value = old; + + #endregion + + /// Return the correct resolve data from the stored data. + public unsafe bool HandleFiles(CollectionResolver resolver, ResourceType type, Utf8GamePath _, out ResolveData resolveData) + { + switch (type) + { + case ResourceType.Scd: + if (_characterSoundData is { IsValueCreated: true, Value.Valid: true }) + { + resolveData = _characterSoundData.Value; + return true; + } + + if (_animationLoadData is { IsValueCreated: true, Value.Valid: true }) + { + resolveData = _animationLoadData.Value; + return true; + } + + break; + case ResourceType.Tmb: + case ResourceType.Pap: + case ResourceType.Avfx: + case ResourceType.Atex: + if (_animationLoadData is { IsValueCreated: true, Value.Valid: true }) + { + resolveData = _animationLoadData.Value; + return true; + } + + break; + } + + var lastObj = LastGameObject; + if (lastObj != nint.Zero) + { + resolveData = resolver.IdentifyCollection((GameObject*)lastObj, true); + return true; + } + + resolveData = ResolveData.Invalid; + return false; + } +} diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs new file mode 100644 index 00000000..b91c5375 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -0,0 +1,54 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Called for some sound effects caused by animations or VFX. +public sealed unsafe class ApricotListenerSoundPlay : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public ApricotListenerSoundPlay(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, true); + } + + public delegate nint Delegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private nint Detour(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6) + { + Penumbra.Log.Excessive($"[Apricot Listener Sound Play] Invoked on 0x{a1:X} with {a2}, {a3}, {a4}, {a5}, {a6}."); + if (a6 == nint.Zero) + return Task.Result.Original(a1, a2, a3, a4, a5, a6); + + // a6 is some instance of Apricot.IInstanceListenner, in some cases we can obtain the associated caster via vfunc 1. + var gameObject = (*(delegate* unmanaged**)a6)[1](a6); + var newData = ResolveData.Invalid; + if (gameObject != null) + { + newData = _collectionResolver.IdentifyCollection(gameObject, true); + } + else + { + // for VfxListenner we can obtain the associated draw object as its first member, + // if the object has different type, drawObject will contain other values or garbage, + // but only be used in a dictionary pointer lookup, so this does not hurt. + var drawObject = ((DrawObject**)a6)[1]; + if (drawObject != null) + newData = _collectionResolver.IdentifyCollection(drawObject, true); + } + + var last = _state.SetAnimationData(newData); + var ret = Task.Result.Original(a1, a2, a3, a4, a5, a6); + _state.RestoreAnimationData(last); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs b/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs new file mode 100644 index 00000000..959165a6 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs @@ -0,0 +1,41 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// +/// Probably used when the base idle animation gets loaded. +/// Make it aware of the correct collection to load the correct pap files. +/// +public sealed unsafe class CharacterBaseLoadAnimation : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly DrawObjectState _drawObjectState; + + public CharacterBaseLoadAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver, + DrawObjectState drawObjectState) + { + _state = state; + _collectionResolver = collectionResolver; + _drawObjectState = drawObjectState; + Task = hooks.CreateHook("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, true); + } + + public delegate void Delegate(DrawObject* drawBase); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject) + { + var lastObj = _state.LastGameObject; + if (lastObj == nint.Zero && _drawObjectState.TryGetValue((nint)drawObject, out var p)) + lastObj = p.Item1; + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)lastObj, true)); + Penumbra.Log.Excessive($"[CharacterBase Load Animation] Invoked on {(nint)drawObject:X}"); + Task.Result.Original(drawObject); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/Dismount.cs b/Penumbra/Interop/Hooks/Animation/Dismount.cs new file mode 100644 index 00000000..8085bcdb --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/Dismount.cs @@ -0,0 +1,44 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Called for some animations when dismounting. +public sealed unsafe class Dismount : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public Dismount(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Dismount", Sigs.Dismount, Detour, true); + } + + public delegate void Delegate(nint a1, nint a2); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(nint a1, nint a2) + { + Penumbra.Log.Excessive($"[Dismount] Invoked on {a1:X} with {a2:X}."); + if (a1 == nint.Zero) + { + Task.Result.Original(a1, a2); + return; + } + + var gameObject = *(GameObject**)(a1 + 8); + if (gameObject == null) + { + Task.Result.Original(a1, a2); + return; + } + + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(gameObject, true)); + Task.Result.Original(a1, a2); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs new file mode 100644 index 00000000..7be420be --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs @@ -0,0 +1,38 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Load a ground-based area VFX. +public sealed unsafe class LoadAreaVfx : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public LoadAreaVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Load Area VFX", Sigs.LoadAreaVfx, Detour, true); + } + + public delegate nint Delegate(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private nint Detour(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3) + { + var newData = caster != null + ? _collectionResolver.IdentifyCollection(caster, true) + : ResolveData.Invalid; + + var last = _state.SetAnimationData(newData); + var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3); + Penumbra.Log.Excessive( + $"[Load Area VFX] Invoked with {vfxId}, [{pos[0]} {pos[1]} {pos[2]}], 0x{(nint)caster:X}, {unk1}, {unk2}, {unk3} -> 0x{ret:X}."); + _state.RestoreAnimationData(last); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs new file mode 100644 index 00000000..af13805d --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs @@ -0,0 +1,34 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Characters load some of their voice lines or whatever with this function. +public sealed unsafe class LoadCharacterSound : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public LoadCharacterSound(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Load Character Sound", + (nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, + true); + } + + public delegate nint Delegate(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private nint Detour(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7) + { + var character = *(GameObject**)(container + 8); + var last = _state.SetSoundData(_collectionResolver.IdentifyCollection(character, true)); + var ret = Task.Result.Original(container, unk1, unk2, unk3, unk4, unk5, unk6, unk7); + Penumbra.Log.Excessive($"[Load Character Sound] Invoked with {container:X} {unk1} {unk2} {unk3} {unk4} {unk5} {unk6} {unk7} -> {ret}."); + _state.RestoreSoundData(last); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs new file mode 100644 index 00000000..240c062e --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs @@ -0,0 +1,65 @@ +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Structs; +using Penumbra.String; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Load a VFX specifically for a character. +public sealed unsafe class LoadCharacterVfx : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly IObjectTable _objects; + + public LoadCharacterVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects) + { + _state = state; + _collectionResolver = collectionResolver; + _objects = objects; + Task = hooks.CreateHook("Load Character VFX", Sigs.LoadCharacterVfx, Detour, true); + } + + public delegate nint Delegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private nint Detour(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4) + { + var newData = ResolveData.Invalid; + if (vfxParams != null && vfxParams->GameObjectId != unchecked((uint)-1)) + { + var obj = vfxParams->GameObjectType switch + { + 0 => _objects.SearchById(vfxParams->GameObjectId), + 2 => _objects[(int)vfxParams->GameObjectId], + 4 => GetOwnedObject(vfxParams->GameObjectId), + _ => null, + }; + newData = obj != null + ? _collectionResolver.IdentifyCollection((GameObject*)obj.Address, true) + : ResolveData.Invalid; + } + + var last = _state.SetAnimationData(newData); + var ret = Task.Result.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4); + Penumbra.Log.Excessive( + $"[Load Character VFX] Invoked with {new ByteString(vfxPath)}, 0x{vfxParams->GameObjectId:X}, {vfxParams->TargetCount}, {unk1}, {unk2}, {unk3}, {unk4} -> 0x{ret:X}."); + _state.RestoreAnimationData(last); + return ret; + } + + /// Search an object by its id, then get its minion/mount/ornament. + private Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject(uint id) + { + var owner = _objects.SearchById(id); + if (owner == null) + return null; + + var idx = ((GameObject*)owner.Address)->ObjectIndex; + return _objects[idx + 1]; + } +} diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs new file mode 100644 index 00000000..2ca8ffe7 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -0,0 +1,71 @@ +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// +/// 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. +/// +public sealed unsafe class LoadTimelineResources : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly ICondition _conditions; + private readonly IObjectTable _objects; + + public LoadTimelineResources(HookManager hooks, GameState state, CollectionResolver collectionResolver, ICondition conditions, + IObjectTable objects) + { + _state = state; + _collectionResolver = collectionResolver; + _conditions = conditions; + _objects = objects; + Task = hooks.CreateHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true); + } + + public delegate ulong Delegate(nint timeline); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private ulong Detour(nint timeline) + { + Penumbra.Log.Excessive($"[Load Timeline Resources] Invoked on {timeline:X}."); + // Do not check timeline loading in cutscenes. + if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78]) + return Task.Result.Original(timeline); + + var last = _state.SetAnimationData(GetDataFromTimeline(_objects, _collectionResolver, timeline)); + var ret = Task.Result.Original(timeline); + _state.RestoreAnimationData(last); + return ret; + } + + /// Use timelines vfuncs to obtain the associated game object. + public static ResolveData GetDataFromTimeline(IObjectTable objects, CollectionResolver resolver, nint timeline) + { + try + { + if (timeline != nint.Zero) + { + var getGameObjectIdx = ((delegate* unmanaged**)timeline)[0][Offsets.GetGameObjectIdxVfunc]; + var idx = getGameObjectIdx(timeline); + if (idx >= 0 && idx < objects.Length) + { + var obj = (GameObject*)objects.GetObjectAddress(idx); + return obj != null ? resolver.IdentifyCollection(obj, true) : ResolveData.Invalid; + } + } + } + catch (Exception e) + { + Penumbra.Log.Error($"Error getting timeline data for 0x{timeline:X}:\n{e}"); + } + + return ResolveData.Invalid; + } +} diff --git a/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs new file mode 100644 index 00000000..491d7662 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs @@ -0,0 +1,30 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +public sealed unsafe class PlayFootstep : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public PlayFootstep(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Play Footstep", Sigs.FootStepSound, Detour, true); + } + + public delegate void Delegate(GameObject* gameObject, int id, int unk); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(GameObject* gameObject, int id, int unk) + { + Penumbra.Log.Excessive($"[Play Footstep] Invoked on 0x{(nint)gameObject:X} with {id}, {unk}."); + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(gameObject, true)); + Task.Result.Original(gameObject, id, unk); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs new file mode 100644 index 00000000..8428f8ff --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs @@ -0,0 +1,35 @@ +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Called when some action timelines update. +public sealed unsafe class ScheduleClipUpdate : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly IObjectTable _objects; + + public ScheduleClipUpdate(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects) + { + _state = state; + _collectionResolver = collectionResolver; + _objects = objects; + Task = hooks.CreateHook("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, true); + } + + public delegate void Delegate(ClipScheduler* x); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(ClipScheduler* clipScheduler) + { + Penumbra.Log.Excessive($"[Schedule Clip Update] Invoked on {(nint)clipScheduler:X}."); + var last = _state.SetAnimationData( + LoadTimelineResources.GetDataFromTimeline(_objects, _collectionResolver, clipScheduler->SchedulerTimeline)); + Task.Result.Original(clipScheduler); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs new file mode 100644 index 00000000..48931d73 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs @@ -0,0 +1,32 @@ +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Seems to load character actions when zoning or changing class, maybe. +public sealed unsafe class SomeActionLoad : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public SomeActionLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Some Action Load", Sigs.LoadSomeAction, Detour, true); + } + + public delegate void Delegate(ActionTimelineManager* timelineManager); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(ActionTimelineManager* timelineManager) + { + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true)); + Penumbra.Log.Excessive($"[Some Action Load] Invoked on 0x{(nint)timelineManager:X}."); + Task.Result.Original(timelineManager); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs b/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs new file mode 100644 index 00000000..5dd8227d --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs @@ -0,0 +1,31 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Called for some animations when mounted or mounting. +public sealed unsafe class SomeMountAnimation : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public SomeMountAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Some Mount Animation", Sigs.UnkMountAnimation, Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject, uint unk1, byte unk2, uint unk3); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject, uint unk1, byte unk2, uint unk3) + { + Penumbra.Log.Excessive($"[Some Mount Animation] Invoked on {(nint)drawObject:X} with {unk1}, {unk2}, {unk3}."); + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(drawObject, true)); + Task.Result.Original(drawObject, unk1, unk2, unk3); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs new file mode 100644 index 00000000..75caacee --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs @@ -0,0 +1,46 @@ +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Unknown what exactly this is, but it seems to load a bunch of paps. +public sealed unsafe class SomePapLoad : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly IObjectTable _objects; + + public SomePapLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects) + { + _state = state; + _collectionResolver = collectionResolver; + _objects = objects; + Task = hooks.CreateHook("Some PAP Load", Sigs.LoadSomePap, Detour, true); + } + + public delegate void Delegate(nint a1, int a2, nint a3, int a4); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(nint a1, int a2, nint a3, int a4) + { + Penumbra.Log.Excessive($"[Some PAP Load] Invoked on 0x{a1:X} with {a2}, {a3}, {a4}."); + var timelinePtr = a1 + Offsets.TimeLinePtr; + if (timelinePtr != nint.Zero) + { + var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3); + if (actorIdx >= 0 && actorIdx < _objects.Length) + { + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), + true)); + Task.Result.Original(a1, a2, a3, a4); + _state.RestoreAnimationData(last); + return; + } + } + + Task.Result.Original(a1, a2, a3, a4); + } +} diff --git a/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs b/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs new file mode 100644 index 00000000..ab4a7201 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs @@ -0,0 +1,31 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Called for some animations when using a Parasol. +public sealed unsafe class SomeParasolAnimation : FastHook +{ + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + + public SomeParasolAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver) + { + _state = state; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("Some Parasol Animation", Sigs.UnkParasolAnimation, Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject, int unk1); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject, int unk1) + { + Penumbra.Log.Excessive($"[Some Mount Animation] Invoked on {(nint)drawObject:X} with {unk1}."); + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(drawObject, true)); + Task.Result.Original(drawObject, unk1); + _state.RestoreAnimationData(last); + } +} diff --git a/Penumbra/Interop/Hooks/CharacterBaseDestructor.cs b/Penumbra/Interop/Hooks/CharacterBaseDestructor.cs new file mode 100644 index 00000000..435ddea6 --- /dev/null +++ b/Penumbra/Interop/Hooks/CharacterBaseDestructor.cs @@ -0,0 +1,49 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.UI.AdvancedWindow; + +namespace Penumbra.Interop.Hooks; + +public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr, IHookService +{ + public enum Priority + { + /// + DrawObjectState = 0, + + /// + MtrlTab = -1000, + } + + public CharacterBaseDestructor(HookManager hooks) + : base("Destroy CharacterBase") + => _task = hooks.CreateHook(Name, Address, Detour, true); + + private readonly Task> _task; + + public nint Address + => (nint)CharacterBase.MemberFunctionPointers.Destroy; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate nint Delegate(CharacterBase* characterBase); + + private nint Detour(CharacterBase* characterBase) + { + Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)characterBase:X}."); + Invoke(characterBase); + return _task.Result.Original(characterBase); + } +} diff --git a/Penumbra/Interop/Hooks/CharacterDestructor.cs b/Penumbra/Interop/Hooks/CharacterDestructor.cs new file mode 100644 index 00000000..4a0e9367 --- /dev/null +++ b/Penumbra/Interop/Hooks/CharacterDestructor.cs @@ -0,0 +1,49 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData; + +namespace Penumbra.Interop.Hooks; + +public sealed unsafe class CharacterDestructor : EventWrapperPtr, IHookService +{ + public enum Priority + { + /// + CutsceneService = 0, + + /// + IdentifiedCollectionCache = 0, + } + + public CharacterDestructor(HookManager hooks) + : base("Character Destructor") + => _task = hooks.CreateHook(Name, Sigs.CharacterDestructor, Detour, true); + + private readonly Task> _task; + + public nint Address + => _task.Result.Address; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate void Delegate(Character* character); + + private void Detour(Character* character) + { + Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)character:X}."); + Invoke(character); + _task.Result.Original(character); + } +} diff --git a/Penumbra/Interop/Hooks/CopyCharacter.cs b/Penumbra/Interop/Hooks/CopyCharacter.cs new file mode 100644 index 00000000..d2e8d816 --- /dev/null +++ b/Penumbra/Interop/Hooks/CopyCharacter.cs @@ -0,0 +1,47 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Classes; +using OtterGui.Services; + +namespace Penumbra.Interop.Hooks; + +public sealed unsafe class CopyCharacter : EventWrapperPtr, IHookService +{ + public enum Priority + { + /// + CutsceneService = 0, + } + + public CopyCharacter(HookManager hooks) + : base("Copy Character") + => _task = hooks.CreateHook(Name, Address, Detour, true); + + private readonly Task> _task; + + public nint Address + => (nint)CharacterSetup.MemberFunctionPointers.CopyFromCharacter; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate ulong Delegate(CharacterSetup* target, Character* source, uint unk); + + private ulong Detour(CharacterSetup* target, Character* source, uint unk) + { + // TODO: update when CS updated. + var character = ((Character**)target)[1]; + Penumbra.Log.Verbose($"[{Name}] Triggered with target: 0x{(nint)target:X}, source : 0x{(nint)source:X} unk: {unk}."); + Invoke(character, source); + return _task.Result.Original(target, source, unk); + } +} diff --git a/Penumbra/Interop/Hooks/CreateCharacterBase.cs b/Penumbra/Interop/Hooks/CreateCharacterBase.cs new file mode 100644 index 00000000..7dbde666 --- /dev/null +++ b/Penumbra/Interop/Hooks/CreateCharacterBase.cs @@ -0,0 +1,74 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData.Structs; + +namespace Penumbra.Interop.Hooks; + +public sealed unsafe class CreateCharacterBase : EventWrapperPtr, IHookService +{ + public enum Priority + { + /// + MetaState = 0, + } + + public CreateCharacterBase(HookManager hooks) + : base("Create CharacterBase") + => _task = hooks.CreateHook(Name, Address, Detour, true); + + private readonly Task> _task; + + public nint Address + => (nint)CharacterBase.MemberFunctionPointers.Create; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate CharacterBase* Delegate(ModelCharaId model, CustomizeArray* customize, CharacterArmor* equipment, byte unk); + + private CharacterBase* Detour(ModelCharaId model, CustomizeArray* customize, CharacterArmor* equipment, byte unk) + { + Penumbra.Log.Verbose($"[{Name}] Triggered with model: {model.Id}, customize: 0x{(nint) customize:X}, equipment: 0x{(nint)equipment:X}, unk: {unk}."); + Invoke(&model, customize, equipment); + var ret = _task.Result.Original(model, customize, equipment, unk); + _postEvent.Invoke(model, customize, equipment, ret); + return ret; + } + + public void Subscribe(ActionPtr234 subscriber, PostEvent.Priority priority) + => _postEvent.Subscribe(subscriber, priority); + + public void Unsubscribe(ActionPtr234 subscriber) + => _postEvent.Unsubscribe(subscriber); + + + private readonly PostEvent _postEvent = new("Created CharacterBase"); + + protected override void Dispose(bool disposing) + { + _postEvent.Dispose(); + } + + public class PostEvent(string name) : EventWrapperPtr234(name) + { + public enum Priority + { + /// + DrawObjectState = 0, + + /// + MetaState = 0, + } + } +} diff --git a/Penumbra/Interop/Hooks/DebugHook.cs b/Penumbra/Interop/Hooks/DebugHook.cs new file mode 100644 index 00000000..67823e94 --- /dev/null +++ b/Penumbra/Interop/Hooks/DebugHook.cs @@ -0,0 +1,43 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Services; + +namespace Penumbra.Interop.Hooks; + +#if DEBUG +public sealed unsafe class DebugHook : IHookService +{ + public const string Signature = ""; + + public DebugHook(HookManager hooks) + { + if (Signature.Length > 0) + _task = hooks.CreateHook("Debug Hook", Signature, Detour, true); + } + + private readonly Task>? _task; + + public nint Address + => _task?.Result.Address ?? nint.Zero; + + public void Enable() + => _task?.Result.Enable(); + + public void Disable() + => _task?.Result.Disable(); + + public Task Awaiter + => _task ?? Task.CompletedTask; + + public bool Finished + => _task?.IsCompletedSuccessfully ?? true; + + private delegate nint Delegate(ResourceHandle* resourceHandle); + + private nint Detour(ResourceHandle* resourceHandle) + { + Penumbra.Log.Information($"[Debug Hook] Triggered with 0x{(nint)resourceHandle:X}."); + return _task!.Result.Original(resourceHandle); + } +} +#endif diff --git a/Penumbra/Interop/Hooks/EnableDraw.cs b/Penumbra/Interop/Hooks/EnableDraw.cs new file mode 100644 index 00000000..884b643d --- /dev/null +++ b/Penumbra/Interop/Hooks/EnableDraw.cs @@ -0,0 +1,48 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.GameData; + +namespace Penumbra.Interop.Hooks; + +/// +/// 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 sealed unsafe class EnableDraw : IHookService +{ + private readonly Task> _task; + private readonly GameState _state; + + public EnableDraw(HookManager hooks, GameState state) + { + _state = state; + _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, true); + } + + private delegate void Delegate(GameObject* gameObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(GameObject* gameObject) + { + _state.QueueGameObject(gameObject); + Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint) gameObject:X}."); + _task.Result.Original.Invoke(gameObject); + _state.DequeueGameObject(); + } + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + public nint Address + => _task.Result.Address; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); +} diff --git a/Penumbra/Interop/Hooks/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/ResourceHandleDestructor.cs new file mode 100644 index 00000000..99eb1c23 --- /dev/null +++ b/Penumbra/Interop/Hooks/ResourceHandleDestructor.cs @@ -0,0 +1,50 @@ +using Dalamud.Hooking; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; + +namespace Penumbra.Interop.Hooks; + +public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr, IHookService +{ + public enum Priority + { + /// + SubfileHelper, + + /// + SkinFixer, + } + + public ResourceHandleDestructor(HookManager hooks) + : base("Destroy ResourceHandle") + => _task = hooks.CreateHook(Name, Sigs.ResourceHandleDestructor, Detour, true); + + private readonly Task> _task; + + public nint Address + => _task.Result.Address; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate nint Delegate(ResourceHandle* resourceHandle); + + private nint Detour(ResourceHandle* resourceHandle) + { + Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)resourceHandle:X}."); + Invoke(resourceHandle); + return _task.Result.Original(resourceHandle); + } +} diff --git a/Penumbra/Interop/Hooks/WeaponReload.cs b/Penumbra/Interop/Hooks/WeaponReload.cs new file mode 100644 index 00000000..b931f8fb --- /dev/null +++ b/Penumbra/Interop/Hooks/WeaponReload.cs @@ -0,0 +1,71 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData.Structs; + +namespace Penumbra.Interop.Hooks; + +public sealed unsafe class WeaponReload : EventWrapperPtr, IHookService +{ + public enum Priority + { + /// + DrawObjectState = 0, + } + + public WeaponReload(HookManager hooks) + : base("Reload Weapon") + => _task = hooks.CreateHook(Name, Address, Detour, true); + + private readonly Task> _task; + + public nint Address + => (nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate void Delegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g); + + private void Detour(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g) + { + var gameObject = drawData->Parent; + Penumbra.Log.Verbose($"[{Name}] Triggered with drawData: 0x{(nint)drawData:X}, {slot}, {weapon}, {d}, {e}, {f}, {g}."); + Invoke(drawData, gameObject, (CharacterWeapon*)(&weapon)); + _task.Result.Original(drawData, slot, weapon, d, e, f, g); + _postEvent.Invoke(drawData, gameObject); + } + + public void Subscribe(ActionPtr subscriber, PostEvent.Priority priority) + => _postEvent.Subscribe(subscriber, priority); + + public void Unsubscribe(ActionPtr subscriber) + => _postEvent.Unsubscribe(subscriber); + + + private readonly PostEvent _postEvent = new("Created CharacterBase"); + + protected override void Dispose(bool disposing) + { + _postEvent.Dispose(); + } + + public class PostEvent(string name) : EventWrapperPtr(name) + { + public enum Priority + { + /// + DrawObjectState = 0, + } + } +} diff --git a/Penumbra/Interop/PathResolving/AnimationHookService.cs b/Penumbra/Interop/PathResolving/AnimationHookService.cs deleted file mode 100644 index d13ef7f2..00000000 --- a/Penumbra/Interop/PathResolving/AnimationHookService.cs +++ /dev/null @@ -1,423 +0,0 @@ -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Hooking; -using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Penumbra.Collections; -using Penumbra.Api.Enums; -using Penumbra.GameData; -using Penumbra.Interop.Structs; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.Util; -using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; - -namespace Penumbra.Interop.PathResolving; - -public unsafe class AnimationHookService : IDisposable -{ - private readonly PerformanceTracker _performance; - private readonly IObjectTable _objects; - private readonly CollectionResolver _collectionResolver; - private readonly DrawObjectState _drawObjectState; - private readonly CollectionResolver _resolver; - private readonly ICondition _conditions; - - private readonly ThreadLocal _animationLoadData = new(() => ResolveData.Invalid, true); - private readonly ThreadLocal _characterSoundData = new(() => ResolveData.Invalid, true); - - public AnimationHookService(PerformanceTracker performance, IObjectTable objects, CollectionResolver collectionResolver, - DrawObjectState drawObjectState, CollectionResolver resolver, ICondition conditions, IGameInteropProvider interop) - { - _performance = performance; - _objects = objects; - _collectionResolver = collectionResolver; - _drawObjectState = drawObjectState; - _resolver = resolver; - _conditions = conditions; - - interop.InitializeFromAttributes(this); - _loadCharacterSoundHook = - interop.HookFromAddress( - (nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound, - LoadCharacterSoundDetour); - - _loadCharacterSoundHook.Enable(); - _loadTimelineResourcesHook.Enable(); - _characterBaseLoadAnimationHook.Enable(); - _loadSomePapHook.Enable(); - _someActionLoadHook.Enable(); - _loadCharacterVfxHook.Enable(); - _loadAreaVfxHook.Enable(); - _scheduleClipUpdateHook.Enable(); - _unkMountAnimationHook.Enable(); - _unkParasolAnimationHook.Enable(); - _dismountHook.Enable(); - _apricotListenerSoundPlayHook.Enable(); - _footStepHook.Enable(); - } - - public bool HandleFiles(ResourceType type, Utf8GamePath _, out ResolveData resolveData) - { - switch (type) - { - case ResourceType.Scd: - if (_characterSoundData is { IsValueCreated: true, Value.Valid: true }) - { - resolveData = _characterSoundData.Value; - return true; - } - - if (_animationLoadData is { IsValueCreated: true, Value.Valid: true }) - { - resolveData = _animationLoadData.Value; - return true; - } - - break; - case ResourceType.Tmb: - case ResourceType.Pap: - case ResourceType.Avfx: - case ResourceType.Atex: - if (_animationLoadData is { IsValueCreated: true, Value.Valid: true }) - { - resolveData = _animationLoadData.Value; - return true; - } - - break; - } - - var lastObj = _drawObjectState.LastGameObject; - if (lastObj != nint.Zero) - { - resolveData = _resolver.IdentifyCollection((GameObject*)lastObj, true); - return true; - } - - resolveData = ResolveData.Invalid; - return false; - } - - public void Dispose() - { - _loadCharacterSoundHook.Dispose(); - _loadTimelineResourcesHook.Dispose(); - _characterBaseLoadAnimationHook.Dispose(); - _loadSomePapHook.Dispose(); - _someActionLoadHook.Dispose(); - _loadCharacterVfxHook.Dispose(); - _loadAreaVfxHook.Dispose(); - _scheduleClipUpdateHook.Dispose(); - _unkMountAnimationHook.Dispose(); - _unkParasolAnimationHook.Dispose(); - _dismountHook.Dispose(); - _apricotListenerSoundPlayHook.Dispose(); - _footStepHook.Dispose(); - } - - /// Characters load some of their voice lines or whatever with this function. - private delegate nint LoadCharacterSound(nint character, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); - - private readonly Hook _loadCharacterSoundHook; - - private nint LoadCharacterSoundDetour(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7) - { - using var performance = _performance.Measure(PerformanceType.LoadSound); - var last = _characterSoundData.Value; - var character = *(GameObject**)(container + 8); - _characterSoundData.Value = _collectionResolver.IdentifyCollection(character, true); - var ret = _loadCharacterSoundHook.Original(container, unk1, unk2, unk3, unk4, unk5, unk6, unk7); - _characterSoundData.Value = last; - return ret; - } - - /// - /// 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(nint timeline); - - [Signature(Sigs.LoadTimelineResources, DetourName = nameof(LoadTimelineResourcesDetour))] - private readonly Hook _loadTimelineResourcesHook = null!; - - private ulong LoadTimelineResourcesDetour(nint timeline) - { - using var performance = _performance.Measure(PerformanceType.TimelineResources); - // Do not check timeline loading in cutscenes. - if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78]) - return _loadTimelineResourcesHook.Original(timeline); - - var last = _animationLoadData.Value; - _animationLoadData.Value = GetDataFromTimeline(timeline); - var ret = _loadTimelineResourcesHook.Original(timeline); - _animationLoadData.Value = last; - 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(nint drawBase); - - [Signature(Sigs.CharacterBaseLoadAnimation, DetourName = nameof(CharacterBaseLoadAnimationDetour))] - private readonly Hook _characterBaseLoadAnimationHook = null!; - - private void CharacterBaseLoadAnimationDetour(nint drawObject) - { - using var performance = _performance.Measure(PerformanceType.LoadCharacterBaseAnimation); - var last = _animationLoadData.Value; - var lastObj = _drawObjectState.LastGameObject; - if (lastObj == nint.Zero && _drawObjectState.TryGetValue(drawObject, out var p)) - lastObj = p.Item1; - _animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)lastObj, true); - _characterBaseLoadAnimationHook.Original(drawObject); - _animationLoadData.Value = last; - } - - /// Unknown what exactly this is but it seems to load a bunch of paps. - private delegate void LoadSomePap(nint a1, int a2, nint a3, int a4); - - [Signature(Sigs.LoadSomePap, DetourName = nameof(LoadSomePapDetour))] - private readonly Hook _loadSomePapHook = null!; - - private void LoadSomePapDetour(nint a1, int a2, nint a3, int a4) - { - using var performance = _performance.Measure(PerformanceType.LoadPap); - var timelinePtr = a1 + Offsets.TimeLinePtr; - var last = _animationLoadData.Value; - if (timelinePtr != nint.Zero) - { - var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3); - if (actorIdx >= 0 && actorIdx < _objects.Length) - _animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), true); - } - - _loadSomePapHook.Original(a1, a2, a3, a4); - _animationLoadData.Value = last; - } - - private delegate void SomeActionLoadDelegate(ActionTimelineManager* timelineManager); - - /// Seems to load character actions when zoning or changing class, maybe. - [Signature(Sigs.LoadSomeAction, DetourName = nameof(SomeActionLoadDetour))] - private readonly Hook _someActionLoadHook = null!; - - private void SomeActionLoadDetour(ActionTimelineManager* timelineManager) - { - using var performance = _performance.Measure(PerformanceType.LoadAction); - var last = _animationLoadData.Value; - _animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true); - _someActionLoadHook.Original(timelineManager); - _animationLoadData.Value = last; - } - - /// Load a VFX specifically for a character. - private delegate nint LoadCharacterVfxDelegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4); - - [Signature(Sigs.LoadCharacterVfx, DetourName = nameof(LoadCharacterVfxDetour))] - private readonly Hook _loadCharacterVfxHook = null!; - - private nint LoadCharacterVfxDetour(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4) - { - using var performance = _performance.Measure(PerformanceType.LoadCharacterVfx); - var last = _animationLoadData.Value; - if (vfxParams != null && vfxParams->GameObjectId != unchecked((uint)-1)) - { - var obj = vfxParams->GameObjectType switch - { - 0 => _objects.SearchById(vfxParams->GameObjectId), - 2 => _objects[(int)vfxParams->GameObjectId], - 4 => GetOwnedObject(vfxParams->GameObjectId), - _ => null, - }; - _animationLoadData.Value = obj != null - ? _collectionResolver.IdentifyCollection((GameObject*)obj.Address, true) - : ResolveData.Invalid; - } - else - { - _animationLoadData.Value = ResolveData.Invalid; - } - - var ret = _loadCharacterVfxHook.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4); - Penumbra.Log.Excessive( - $"Load Character VFX: {new ByteString(vfxPath)} 0x{vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> " - + $"0x{ret:X} {_animationLoadData.Value.ModCollection.Name} {_animationLoadData.Value.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}"); - _animationLoadData.Value = last; - return ret; - } - - /// Load a ground-based area VFX. - private delegate nint LoadAreaVfxDelegate(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3); - - [Signature(Sigs.LoadAreaVfx, DetourName = nameof(LoadAreaVfxDetour))] - private readonly Hook _loadAreaVfxHook = null!; - - private nint LoadAreaVfxDetour(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3) - { - using var performance = _performance.Measure(PerformanceType.LoadAreaVfx); - var last = _animationLoadData.Value; - _animationLoadData.Value = caster != null - ? _collectionResolver.IdentifyCollection(caster, true) - : ResolveData.Invalid; - - var ret = _loadAreaVfxHook.Original(vfxId, pos, caster, unk1, unk2, unk3); - Penumbra.Log.Excessive( - $"Load Area VFX: {vfxId}, {pos[0]} {pos[1]} {pos[2]} {(caster != null ? new ByteString(caster->GetName()).ToString() : "Unknown")} {unk1} {unk2} {unk3}" - + $" -> {ret:X} {_animationLoadData.Value.ModCollection.Name} {_animationLoadData.Value.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}"); - _animationLoadData.Value = last; - return ret; - } - - - /// Called when some action timelines update. - private delegate void ScheduleClipUpdate(ClipScheduler* x); - - [Signature(Sigs.ScheduleClipUpdate, DetourName = nameof(ScheduleClipUpdateDetour))] - private readonly Hook _scheduleClipUpdateHook = null!; - - private void ScheduleClipUpdateDetour(ClipScheduler* x) - { - using var performance = _performance.Measure(PerformanceType.ScheduleClipUpdate); - var last = _animationLoadData.Value; - var timeline = x->SchedulerTimeline; - _animationLoadData.Value = GetDataFromTimeline(timeline); - _scheduleClipUpdateHook.Original(x); - _animationLoadData.Value = last; - } - - /// Search an object by its id, then get its minion/mount/ornament. - private Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject(uint id) - { - var owner = _objects.SearchById(id); - if (owner == null) - return null; - - var idx = ((GameObject*)owner.Address)->ObjectIndex; - return _objects[idx + 1]; - } - - /// Use timelines vfuncs to obtain the associated game object. - private ResolveData GetDataFromTimeline(nint timeline) - { - try - { - if (timeline != nint.Zero) - { - var getGameObjectIdx = ((delegate* unmanaged**)timeline)[0][Offsets.GetGameObjectIdxVfunc]; - var idx = getGameObjectIdx(timeline); - if (idx >= 0 && idx < _objects.Length) - { - var obj = (GameObject*)_objects.GetObjectAddress(idx); - return obj != null ? _collectionResolver.IdentifyCollection(obj, true) : ResolveData.Invalid; - } - } - } - catch (Exception e) - { - Penumbra.Log.Error($"Error getting timeline data for 0x{timeline:X}:\n{e}"); - } - - return ResolveData.Invalid; - } - - private delegate void UnkMountAnimationDelegate(DrawObject* drawObject, uint unk1, byte unk2, uint unk3); - - [Signature(Sigs.UnkMountAnimation, DetourName = nameof(UnkMountAnimationDetour))] - private readonly Hook _unkMountAnimationHook = null!; - - private void UnkMountAnimationDetour(DrawObject* drawObject, uint unk1, byte unk2, uint unk3) - { - var last = _animationLoadData.Value; - _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); - _unkMountAnimationHook.Original(drawObject, unk1, unk2, unk3); - _animationLoadData.Value = last; - } - - private delegate void UnkParasolAnimationDelegate(DrawObject* drawObject, int unk1); - - [Signature(Sigs.UnkParasolAnimation, DetourName = nameof(UnkParasolAnimationDetour))] - private readonly Hook _unkParasolAnimationHook = null!; - - private void UnkParasolAnimationDetour(DrawObject* drawObject, int unk1) - { - var last = _animationLoadData.Value; - _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); - _unkParasolAnimationHook.Original(drawObject, unk1); - _animationLoadData.Value = last; - } - - [Signature(Sigs.Dismount, DetourName = nameof(DismountDetour))] - private readonly Hook _dismountHook = null!; - - private delegate void DismountDelegate(nint a1, nint a2); - - private void DismountDetour(nint a1, nint a2) - { - if (a1 == nint.Zero) - { - _dismountHook.Original(a1, a2); - return; - } - - var gameObject = *(GameObject**)(a1 + 8); - if (gameObject == null) - { - _dismountHook.Original(a1, a2); - return; - } - - var last = _animationLoadData.Value; - _animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true); - _dismountHook.Original(a1, a2); - _animationLoadData.Value = last; - } - - [Signature(Sigs.ApricotListenerSoundPlay, DetourName = nameof(ApricotListenerSoundPlayDetour))] - private readonly Hook _apricotListenerSoundPlayHook = null!; - - private delegate nint ApricotListenerSoundPlayDelegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6); - - private nint ApricotListenerSoundPlayDetour(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6) - { - if (a6 == nint.Zero) - return _apricotListenerSoundPlayHook!.Original(a1, a2, a3, a4, a5, a6); - - var last = _animationLoadData.Value; - // a6 is some instance of Apricot.IInstanceListenner, in some cases we can obtain the associated caster via vfunc 1. - var gameObject = (*(delegate* unmanaged**)a6)[1](a6); - if (gameObject != null) - { - _animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true); - } - else - { - // for VfxListenner we can obtain the associated draw object as its first member, - // if the object has different type, drawObject will contain other values or garbage, - // but only be used in a dictionary pointer lookup, so this does not hurt. - var drawObject = ((DrawObject**)a6)[1]; - if (drawObject != null) - _animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true); - } - - var ret = _apricotListenerSoundPlayHook!.Original(a1, a2, a3, a4, a5, a6); - _animationLoadData.Value = last; - return ret; - } - - private delegate void FootStepDelegate(GameObject* gameObject, int id, int unk); - - [Signature(Sigs.FootStepSound, DetourName = nameof(FootStepDetour))] - private readonly Hook _footStepHook = null!; - - private void FootStepDetour(GameObject* gameObject, int id, int unk) - { - var last = _animationLoadData.Value; - _animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true); - _footStepHook.Original(gameObject, id, unk); - _animationLoadData.Value = last; - } -} diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index fe51a5cd..c649147a 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,11 +1,11 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; -using Penumbra.Services; using Penumbra.Util; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; @@ -13,70 +13,51 @@ using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace Penumbra.Interop.PathResolving; -public unsafe class CollectionResolver +public sealed unsafe class CollectionResolver( + PerformanceTracker performance, + IdentifiedCollectionCache cache, + IClientState clientState, + IGameGui gameGui, + ActorManager actors, + CutsceneService cutscenes, + Configuration config, + CollectionManager collectionManager, + TempCollectionManager tempCollections, + DrawObjectState drawObjectState, + HumanModelList humanModels) + : IService { - private readonly PerformanceTracker _performance; - private readonly IdentifiedCollectionCache _cache; - private readonly HumanModelList _humanModels; - - private readonly IClientState _clientState; - private readonly IGameGui _gameGui; - private readonly ActorManager _actors; - private readonly CutsceneService _cutscenes; - - private readonly Configuration _config; - private readonly CollectionManager _collectionManager; - private readonly TempCollectionManager _tempCollections; - private readonly DrawObjectState _drawObjectState; - - public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, IClientState clientState, IGameGui gameGui, - ActorManager actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager, - TempCollectionManager tempCollections, DrawObjectState drawObjectState, HumanModelList humanModels) - { - _performance = performance; - _cache = cache; - _clientState = clientState; - _gameGui = gameGui; - _actors = actors; - _cutscenes = cutscenes; - _config = config; - _collectionManager = collectionManager; - _tempCollections = tempCollections; - _drawObjectState = drawObjectState; - _humanModels = humanModels; - } - /// /// Get the collection applying to the current player character /// or the Yourself or Default collection if no player exists. /// public ModCollection PlayerCollection() { - using var performance = _performance.Measure(PerformanceType.IdentifyCollection); - var gameObject = (GameObject*)(_clientState.LocalPlayer?.Address ?? nint.Zero); + using var performance1 = performance.Measure(PerformanceType.IdentifyCollection); + var gameObject = (GameObject*)(clientState.LocalPlayer?.Address ?? nint.Zero); if (gameObject == null) - return _collectionManager.Active.ByType(CollectionType.Yourself) - ?? _collectionManager.Active.Default; + return collectionManager.Active.ByType(CollectionType.Yourself) + ?? collectionManager.Active.Default; - var player = _actors.GetCurrentPlayer(); + var player = actors.GetCurrentPlayer(); var _ = false; return CollectionByIdentifier(player) ?? CheckYourself(player, gameObject) ?? CollectionByAttributes(gameObject, ref _) - ?? _collectionManager.Active.Default; + ?? collectionManager.Active.Default; } /// Identify the correct collection for a game object. public ResolveData IdentifyCollection(GameObject* gameObject, bool useCache) { - using var t = _performance.Measure(PerformanceType.IdentifyCollection); + using var t = performance.Measure(PerformanceType.IdentifyCollection); if (gameObject == null) - return _collectionManager.Active.Default.ToResolveData(); + return collectionManager.Active.Default.ToResolveData(); try { - if (useCache && _cache.TryGetValue(gameObject, out var data)) + if (useCache && cache.TryGetValue(gameObject, out var data)) return data; if (LoginScreen(gameObject, out data)) @@ -90,26 +71,26 @@ public unsafe class CollectionResolver catch (Exception ex) { Penumbra.Log.Error($"Error identifying collection:\n{ex}"); - return _collectionManager.Active.Default.ToResolveData(gameObject); + return collectionManager.Active.Default.ToResolveData(gameObject); } } /// Identify the correct collection for the last created game object. public ResolveData IdentifyLastGameObjectCollection(bool useCache) - => IdentifyCollection((GameObject*)_drawObjectState.LastGameObject, useCache); + => IdentifyCollection((GameObject*)drawObjectState.LastGameObject, useCache); /// Identify the correct collection for a draw object. public ResolveData IdentifyCollection(DrawObject* drawObject, bool useCache) { - var obj = (GameObject*)(_drawObjectState.TryGetValue((nint)drawObject, out var gameObject) + var obj = (GameObject*)(drawObjectState.TryGetValue((nint)drawObject, out var gameObject) ? gameObject.Item1 - : _drawObjectState.LastGameObject); + : drawObjectState.LastGameObject); return IdentifyCollection(obj, useCache); } /// Return whether the given ModelChara id refers to a human-type model. public bool IsModelHuman(uint modelCharaId) - => _humanModels.IsHuman(modelCharaId); + => humanModels.IsHuman(modelCharaId); /// Return whether the given character has a human model. public bool IsModelHuman(Character* character) @@ -124,36 +105,36 @@ public unsafe class CollectionResolver { // Also check for empty names because sometimes named other characters // might be loaded before being officially logged in. - if (_clientState.IsLoggedIn || gameObject->Name[0] != '\0') + if (clientState.IsLoggedIn || gameObject->Name[0] != '\0') { ret = ResolveData.Invalid; return false; } var notYetReady = false; - var collection = _collectionManager.Active.ByType(CollectionType.Yourself) + var collection = collectionManager.Active.ByType(CollectionType.Yourself) ?? CollectionByAttributes(gameObject, ref notYetReady) - ?? _collectionManager.Active.Default; - ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject); + ?? collectionManager.Active.Default; + ret = notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, ActorIdentifier.Invalid, gameObject); return true; } /// Used if at the aesthetician. The relevant actor is yourself, so use player collection when possible. private bool Aesthetician(GameObject* gameObject, out ResolveData ret) { - if (_gameGui.GetAddonByName("ScreenLog") != IntPtr.Zero) + if (gameGui.GetAddonByName("ScreenLog") != IntPtr.Zero) { ret = ResolveData.Invalid; return false; } - var player = _actors.GetCurrentPlayer(); + var player = actors.GetCurrentPlayer(); var notYetReady = false; var collection = (player.IsValid ? CollectionByIdentifier(player) : null) - ?? _collectionManager.Active.ByType(CollectionType.Yourself) + ?? collectionManager.Active.ByType(CollectionType.Yourself) ?? CollectionByAttributes(gameObject, ref notYetReady) - ?? _collectionManager.Active.Default; - ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject); + ?? collectionManager.Active.Default; + ret = notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, ActorIdentifier.Invalid, gameObject); return true; } @@ -163,12 +144,12 @@ public unsafe class CollectionResolver /// private ResolveData DefaultState(GameObject* gameObject) { - var identifier = _actors.FromObject(gameObject, out var owner, true, false, false); + var identifier = actors.FromObject(gameObject, out var owner, true, false, false); if (identifier.Type is IdentifierType.Special) { - (identifier, var type) = _collectionManager.Active.Individuals.ConvertSpecialIdentifier(identifier); - if (_config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect) - return _cache.Set(ModCollection.Empty, identifier, gameObject); + (identifier, var type) = collectionManager.Active.Individuals.ConvertSpecialIdentifier(identifier); + if (config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect) + return cache.Set(ModCollection.Empty, identifier, gameObject); } var notYetReady = false; @@ -176,15 +157,15 @@ public unsafe class CollectionResolver ?? CheckYourself(identifier, gameObject) ?? CollectionByAttributes(gameObject, ref notYetReady) ?? CheckOwnedCollection(identifier, owner, ref notYetReady) - ?? _collectionManager.Active.Default; + ?? collectionManager.Active.Default; - return notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, identifier, gameObject); + return notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, identifier, gameObject); } /// Check both temporary and permanent character collections. Temporary first. private ModCollection? CollectionByIdentifier(ActorIdentifier identifier) - => _tempCollections.Collections.TryGetCollection(identifier, out var collection) - || _collectionManager.Active.Individuals.TryGetCollection(identifier, out collection) + => tempCollections.Collections.TryGetCollection(identifier, out var collection) + || collectionManager.Active.Individuals.TryGetCollection(identifier, out collection) ? collection : null; @@ -192,9 +173,9 @@ public unsafe class CollectionResolver private ModCollection? CheckYourself(ActorIdentifier identifier, GameObject* actor) { if (actor->ObjectIndex == 0 - || _cutscenes.GetParentIndex(actor->ObjectIndex) == 0 - || identifier.Equals(_actors.GetCurrentPlayer())) - return _collectionManager.Active.ByType(CollectionType.Yourself); + || cutscenes.GetParentIndex(actor->ObjectIndex) == 0 + || identifier.Equals(actors.GetCurrentPlayer())) + return collectionManager.Active.ByType(CollectionType.Yourself); return null; } @@ -219,8 +200,8 @@ public unsafe class CollectionResolver var bodyType = character->DrawData.CustomizeData[2]; var collection = bodyType switch { - 3 => _collectionManager.Active.ByType(CollectionType.NonPlayerElderly), - 4 => _collectionManager.Active.ByType(CollectionType.NonPlayerChild), + 3 => collectionManager.Active.ByType(CollectionType.NonPlayerElderly), + 4 => collectionManager.Active.ByType(CollectionType.NonPlayerChild), _ => null, }; if (collection != null) @@ -231,18 +212,18 @@ public unsafe class CollectionResolver var isNpc = actor->ObjectKind != (byte)ObjectKind.Player; var type = CollectionTypeExtensions.FromParts(race, gender, isNpc); - collection = _collectionManager.Active.ByType(type); - collection ??= _collectionManager.Active.ByType(CollectionTypeExtensions.FromParts(gender, isNpc)); + collection = collectionManager.Active.ByType(type); + collection ??= collectionManager.Active.ByType(CollectionTypeExtensions.FromParts(gender, isNpc)); return collection; } /// Get the collection applying to the owner if it is available. private ModCollection? CheckOwnedCollection(ActorIdentifier identifier, GameObject* owner, ref bool notYetReady) { - if (identifier.Type != IdentifierType.Owned || !_config.UseOwnerNameForCharacterCollection || owner == null) + if (identifier.Type != IdentifierType.Owned || !config.UseOwnerNameForCharacterCollection || owner == null) return null; - var id = _actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id, + var id = actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); return CheckYourself(id, owner) diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 18c016b9..c7b24bd7 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -1,31 +1,34 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Services; using Penumbra.GameData.Enums; -using Penumbra.Interop.Services; +using Penumbra.Interop.Hooks; namespace Penumbra.Interop.PathResolving; -public class CutsceneService : IDisposable +public sealed class CutsceneService : IService, IDisposable { public const int CutsceneStartIdx = (int)ScreenActor.CutsceneStart; public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd; public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx; - private readonly GameEventManager _events; - private readonly IObjectTable _objects; - private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); + private readonly IObjectTable _objects; + private readonly CopyCharacter _copyCharacter; + private readonly CharacterDestructor _characterDestructor; + private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); public IEnumerable> Actors => Enumerable.Range(CutsceneStartIdx, CutsceneSlots) .Where(i => _objects[i] != null) .Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!)); - public unsafe CutsceneService(IObjectTable objects, GameEventManager events) + public unsafe CutsceneService(IObjectTable objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor) { - _objects = objects; - _events = events; - _events.CopyCharacter += OnCharacterCopy; - _events.CharacterDestructor += OnCharacterDestructor; + _objects = objects; + _copyCharacter = copyCharacter; + _characterDestructor = characterDestructor; + _copyCharacter.Subscribe(OnCharacterCopy, CopyCharacter.Priority.CutsceneService); + _characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.CutsceneService); } /// @@ -57,8 +60,8 @@ public class CutsceneService : IDisposable public unsafe void Dispose() { - _events.CopyCharacter -= OnCharacterCopy; - _events.CharacterDestructor -= OnCharacterDestructor; + _copyCharacter.Unsubscribe(OnCharacterCopy); + _characterDestructor.Unsubscribe(OnCharacterDestructor); } private unsafe void OnCharacterDestructor(Character* character) diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 9726d84c..19c0fd10 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -1,35 +1,39 @@ -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Penumbra.GameData; -using Penumbra.Interop.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Interop.Hooks; using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; +using Penumbra.GameData.Structs; namespace Penumbra.Interop.PathResolving; -public class DrawObjectState : IDisposable, IReadOnlyDictionary +public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary, IService { - private readonly IObjectTable _objects; - private readonly GameEventManager _gameEvents; + private readonly IObjectTable _objects; + private readonly CreateCharacterBase _createCharacterBase; + private readonly WeaponReload _weaponReload; + private readonly CharacterBaseDestructor _characterBaseDestructor; + private readonly GameState _gameState; - private readonly Dictionary _drawObjectToGameObject = new(); - - private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); + private readonly Dictionary _drawObjectToGameObject = []; public nint LastGameObject - => _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero; + => _gameState.LastGameObject; - public DrawObjectState(IObjectTable objects, GameEventManager gameEvents, IGameInteropProvider interop) + public unsafe DrawObjectState(IObjectTable objects, CreateCharacterBase createCharacterBase, WeaponReload weaponReload, + CharacterBaseDestructor characterBaseDestructor, GameState gameState) { - interop.InitializeFromAttributes(this); - _enableDrawHook.Enable(); - _objects = objects; - _gameEvents = gameEvents; - _gameEvents.WeaponReloading += OnWeaponReloading; - _gameEvents.WeaponReloaded += OnWeaponReloaded; - _gameEvents.CharacterBaseCreated += OnCharacterBaseCreated; - _gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor; + _objects = objects; + _createCharacterBase = createCharacterBase; + _weaponReload = weaponReload; + _characterBaseDestructor = characterBaseDestructor; + _gameState = gameState; + _weaponReload.Subscribe(OnWeaponReloading, WeaponReload.Priority.DrawObjectState); + _weaponReload.Subscribe(OnWeaponReloaded, WeaponReload.PostEvent.Priority.DrawObjectState); + _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.DrawObjectState); + _characterBaseDestructor.Subscribe(OnCharacterBaseDestructor, CharacterBaseDestructor.Priority.DrawObjectState); InitializeDrawObjects(); } @@ -57,32 +61,32 @@ public class DrawObjectState : IDisposable, IReadOnlyDictionary Values => _drawObjectToGameObject.Values; - public void Dispose() + public unsafe void Dispose() { - _gameEvents.WeaponReloading -= OnWeaponReloading; - _gameEvents.WeaponReloaded -= OnWeaponReloaded; - _gameEvents.CharacterBaseCreated -= OnCharacterBaseCreated; - _gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; - _enableDrawHook.Dispose(); + _weaponReload.Unsubscribe(OnWeaponReloading); + _weaponReload.Unsubscribe(OnWeaponReloaded); + _createCharacterBase.Unsubscribe(OnCharacterBaseCreated); + _characterBaseDestructor.Unsubscribe(OnCharacterBaseDestructor); } - private void OnWeaponReloading(nint _, nint gameObject) - => _lastGameObject.Value!.Enqueue(gameObject); + private unsafe void OnWeaponReloading(DrawDataContainer* _, Character* character, CharacterWeapon* _2) + => _gameState.QueueGameObject((nint)character); - private unsafe void OnWeaponReloaded(nint _, nint gameObject) + private unsafe void OnWeaponReloaded(DrawDataContainer* _, Character* character) { - _lastGameObject.Value!.Dequeue(); - IterateDrawObjectTree((Object*)((GameObject*)gameObject)->DrawObject, gameObject, false, false); + _gameState.DequeueGameObject(); + IterateDrawObjectTree((Object*)character->GameObject.DrawObject, (nint)character, false, false); } - private void OnCharacterBaseDestructor(nint characterBase) - => _drawObjectToGameObject.Remove(characterBase); + private unsafe void OnCharacterBaseDestructor(CharacterBase* characterBase) + => _drawObjectToGameObject.Remove((nint)characterBase); - private void OnCharacterBaseCreated(uint modelCharaId, nint customize, nint equipment, nint drawObject) + private unsafe void OnCharacterBaseCreated(ModelCharaId modelCharaId, CustomizeArray* customize, CharacterArmor* equipment, + CharacterBase* drawObject) { var gameObject = LastGameObject; if (gameObject != nint.Zero) - _drawObjectToGameObject[drawObject] = (gameObject, false); + _drawObjectToGameObject[(nint)drawObject] = (gameObject, false); } /// @@ -123,20 +127,4 @@ public class DrawObjectState : IDisposable, IReadOnlyDictionaryPreviousSiblingObject; } } - - /// - /// 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(nint gameObject); - - [Signature(Sigs.EnableDraw, DetourName = nameof(EnableDrawDetour))] - private readonly Hook _enableDrawHook = null!; - - private void EnableDrawDetour(nint gameObject) - { - _lastGameObject.Value!.Enqueue(gameObject); - _enableDrawHook.Original.Invoke(gameObject); - _lastGameObject.Value!.TryDequeue(out _); - } } diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 73c20ab9..3e7171f8 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -5,7 +5,7 @@ using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.GameData.Actors; -using Penumbra.Interop.Services; +using Penumbra.Interop.Hooks; using Penumbra.Services; namespace Penumbra.Interop.PathResolving; @@ -13,20 +13,20 @@ namespace Penumbra.Interop.PathResolving; public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> { private readonly CommunicatorService _communicator; - private readonly GameEventManager _events; + private readonly CharacterDestructor _characterDestructor; private readonly IClientState _clientState; private readonly Dictionary _cache = new(317); private bool _dirty; - public IdentifiedCollectionCache(IClientState clientState, CommunicatorService communicator, GameEventManager events) + public IdentifiedCollectionCache(IClientState clientState, CommunicatorService communicator, CharacterDestructor characterDestructor) { - _clientState = clientState; - _communicator = communicator; - _events = events; + _clientState = clientState; + _communicator = communicator; + _characterDestructor = characterDestructor; _communicator.CollectionChange.Subscribe(CollectionChangeClear, CollectionChange.Priority.IdentifiedCollectionCache); _clientState.TerritoryChanged += TerritoryClear; - _events.CharacterDestructor += OnCharacterDestruct; + _characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.IdentifiedCollectionCache); } public ResolveData Set(ModCollection collection, ActorIdentifier identifier, GameObject* data) @@ -62,7 +62,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A { _communicator.CollectionChange.Unsubscribe(CollectionChangeClear); _clientState.TerritoryChanged -= TerritoryClear; - _events.CharacterDestructor -= OnCharacterDestruct; + _characterDestructor.Unsubscribe(OnCharacterDestructor); } public IEnumerator<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> GetEnumerator() @@ -88,6 +88,6 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A private void TerritoryClear(ushort _2) => _dirty = _cache.Count > 0; - private void OnCharacterDestruct(Character* character) + private void OnCharacterDestructor(Character* character) => _cache.Remove((nint)character); } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index c1e0bb80..9ef291c7 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -9,6 +9,8 @@ using Penumbra.Collections; using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Hooks; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Services; @@ -52,24 +54,24 @@ public unsafe class MetaState : IDisposable private readonly PerformanceTracker _performance; private readonly CollectionResolver _collectionResolver; private readonly ResourceLoader _resources; - private readonly GameEventManager _gameEventManager; private readonly CharacterUtility _characterUtility; + private readonly CreateCharacterBase _createCharacterBase; private ResolveData _lastCreatedCollection = ResolveData.Invalid; private ResolveData _customizeChangeCollection = ResolveData.Invalid; private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver, - ResourceLoader resources, GameEventManager gameEventManager, CharacterUtility characterUtility, Configuration config, + ResourceLoader resources, CreateCharacterBase createCharacterBase, CharacterUtility characterUtility, Configuration config, IGameInteropProvider interop) { - _performance = performance; - _communicator = communicator; - _collectionResolver = collectionResolver; - _resources = resources; - _gameEventManager = gameEventManager; - _characterUtility = characterUtility; - _config = config; + _performance = performance; + _communicator = communicator; + _collectionResolver = collectionResolver; + _resources = resources; + _createCharacterBase = createCharacterBase; + _characterUtility = characterUtility; + _config = config; interop.InitializeFromAttributes(this); _calculateHeightHook = interop.HookFromAddress((nint)Character.MemberFunctionPointers.CalculateHeight, CalculateHeightDetour); @@ -81,8 +83,8 @@ public unsafe class MetaState : IDisposable _rspSetupCharacterHook.Enable(); _changeCustomize.Enable(); _calculateHeightHook.Enable(); - _gameEventManager.CreatingCharacterBase += OnCreatingCharacterBase; - _gameEventManager.CharacterBaseCreated += OnCharacterBaseCreated; + _createCharacterBase.Subscribe(OnCreatingCharacterBase, CreateCharacterBase.Priority.MetaState); + _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.MetaState); } public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData) @@ -124,31 +126,31 @@ public unsafe class MetaState : IDisposable _rspSetupCharacterHook.Dispose(); _changeCustomize.Dispose(); _calculateHeightHook.Dispose(); - _gameEventManager.CreatingCharacterBase -= OnCreatingCharacterBase; - _gameEventManager.CharacterBaseCreated -= OnCharacterBaseCreated; + _createCharacterBase.Unsubscribe(OnCreatingCharacterBase); + _createCharacterBase.Unsubscribe(OnCharacterBaseCreated); } - private void OnCreatingCharacterBase(nint modelCharaId, nint customize, nint equipData) + private void OnCreatingCharacterBase(ModelCharaId* modelCharaId, CustomizeArray* customize, CharacterArmor* equipData) { _lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true); if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection.Name, modelCharaId, customize, equipData); + _lastCreatedCollection.ModCollection.Name, (nint) modelCharaId, (nint) customize, (nint) equipData); var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, - UsesDecal(*(uint*)modelCharaId, customize)); + UsesDecal(*(uint*)modelCharaId, (nint) customize)); var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. _characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp); } - private void OnCharacterBaseCreated(uint _1, nint _2, nint _3, nint drawObject) + private void OnCharacterBaseCreated(ModelCharaId _1, CustomizeArray* _2, CharacterArmor* _3, CharacterBase* drawObject) { _characterBaseCreateMetaChanges.Dispose(); _characterBaseCreateMetaChanges = DisposableContainer.Empty; - if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != nint.Zero) + if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != null) _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection, drawObject); + _lastCreatedCollection.ModCollection, (nint)drawObject); _lastCreatedCollection = ResolveData.Invalid; } diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 6db97b63..7c16b97b 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -18,26 +18,28 @@ public class PathResolver : IDisposable private readonly TempCollectionManager _tempCollections; private readonly ResourceLoader _loader; - private readonly AnimationHookService _animationHookService; - private readonly SubfileHelper _subfileHelper; - private readonly PathState _pathState; - private readonly MetaState _metaState; + private readonly SubfileHelper _subfileHelper; + private readonly PathState _pathState; + private readonly MetaState _metaState; + private readonly GameState _gameState; + private readonly CollectionResolver _collectionResolver; public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, - TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper, - PathState pathState, MetaState metaState) + TempCollectionManager tempCollections, ResourceLoader loader, SubfileHelper subfileHelper, + PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState) { - _performance = performance; - _config = config; - _collectionManager = collectionManager; - _tempCollections = tempCollections; - _animationHookService = animationHookService; - _subfileHelper = subfileHelper; - _pathState = pathState; - _metaState = metaState; - _loader = loader; - _loader.ResolvePath = ResolvePath; - _loader.FileLoaded += ImcLoadResource; + _performance = performance; + _config = config; + _collectionManager = collectionManager; + _tempCollections = tempCollections; + _subfileHelper = subfileHelper; + _pathState = pathState; + _metaState = metaState; + _gameState = gameState; + _collectionResolver = collectionResolver; + _loader = loader; + _loader.ResolvePath = ResolvePath; + _loader.FileLoaded += ImcLoadResource; } /// Obtain a temporary or permanent collection by name. @@ -98,7 +100,7 @@ public class PathResolver : IDisposable // A potential next request will add the path anew. var nonDefault = _subfileHelper.HandleSubFiles(type, out var resolveData) || _pathState.Consume(gamePath.Path, out resolveData) - || _animationHookService.HandleFiles(type, gamePath, out resolveData) + || _gameState.HandleFiles(_collectionResolver, type, gamePath, out resolveData) || _metaState.HandleDecalFile(type, gamePath, out resolveData); if (!nonDefault || !resolveData.Valid) resolveData = _collectionManager.Active.Default.ToResolveData(); diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 3a60450d..370118ea 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -4,6 +4,7 @@ using Dalamud.Utility.Signatures; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData; +using Penumbra.Interop.Hooks; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; @@ -21,30 +22,30 @@ namespace Penumbra.Interop.PathResolving; /// public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection> { - private readonly PerformanceTracker _performance; - private readonly ResourceLoader _loader; - private readonly GameEventManager _events; - private readonly CommunicatorService _communicator; + private readonly PerformanceTracker _performance; + private readonly ResourceLoader _loader; + private readonly ResourceHandleDestructor _resourceHandleDestructor; + private readonly CommunicatorService _communicator; private readonly ThreadLocal _mtrlData = new(() => ResolveData.Invalid); private readonly ThreadLocal _avfxData = new(() => ResolveData.Invalid); private readonly ConcurrentDictionary _subFileCollection = new(); - public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, GameEventManager events, CommunicatorService communicator, IGameInteropProvider interop) + public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, CommunicatorService communicator, IGameInteropProvider interop, ResourceHandleDestructor resourceHandleDestructor) { interop.InitializeFromAttributes(this); - _performance = performance; - _loader = loader; - _events = events; - _communicator = communicator; + _performance = performance; + _loader = loader; + _communicator = communicator; + _resourceHandleDestructor = resourceHandleDestructor; _loadMtrlShpkHook.Enable(); _loadMtrlTexHook.Enable(); _apricotResourceLoadHook.Enable(); _loader.ResourceLoaded += SubfileContainerRequested; - _events.ResourceHandleDestructor += ResourceDestroyed; + _resourceHandleDestructor.Subscribe(ResourceDestroyed, ResourceHandleDestructor.Priority.SubfileHelper); } @@ -105,7 +106,7 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection((nint)CharacterSetup.MemberFunctionPointers.CopyFromCharacter, CopyCharacterDetour); - _characterBaseCreateHook = - interop.HookFromAddress((nint)CharacterBase.MemberFunctionPointers.Create, CharacterBaseCreateDetour); - _characterBaseDestructorHook = - interop.HookFromAddress((nint)CharacterBase.MemberFunctionPointers.Destroy, - CharacterBaseDestructorDetour); - _weaponReloadHook = - interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, WeaponReloadDetour); - _characterDtorHook.Enable(); - _copyCharacterHook.Enable(); - _resourceHandleDestructorHook.Enable(); - _characterBaseCreateHook.Enable(); - _characterBaseDestructorHook.Enable(); - _weaponReloadHook.Enable(); - EnableDebugHook(); - Penumbra.Log.Verbose($"{Prefix} Created."); - } - - public void Dispose() - { - _characterDtorHook.Dispose(); - _copyCharacterHook.Dispose(); - _resourceHandleDestructorHook.Dispose(); - _characterBaseCreateHook.Dispose(); - _characterBaseDestructorHook.Dispose(); - _weaponReloadHook.Dispose(); - DisposeDebugHook(); - Penumbra.Log.Verbose($"{Prefix} Disposed."); - } - - #region Character Destructor - - private delegate void CharacterDestructorDelegate(Character* character); - - [Signature(Sigs.CharacterDestructor, DetourName = nameof(CharacterDestructorDetour))] - private readonly Hook _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($"{Prefix} Error in {nameof(CharacterDestructor)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - Penumbra.Log.Verbose($"{Prefix} {nameof(CharacterDestructor)} triggered with 0x{(nint)character:X}."); - _characterDtorHook.Original(character); - } - - public delegate void CharacterDestructorEvent(Character* character); - - #endregion - - #region Copy Character - - private delegate ulong CopyCharacterDelegate(CharacterSetup* target, GameObject* source, uint unk); - - private readonly Hook _copyCharacterHook; - - private ulong CopyCharacterDetour(CharacterSetup* target, GameObject* source, uint unk) - { - // TODO: update when CS updated. - var character = ((Character**)target)[1]; - if (CopyCharacter != null) - foreach (var subscriber in CopyCharacter.GetInvocationList()) - { - try - { - ((CopyCharacterEvent)subscriber).Invoke(character, (Character*)source); - } - catch (Exception ex) - { - Penumbra.Log.Error( - $"{Prefix} Error in {nameof(CopyCharacter)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - Penumbra.Log.Verbose( - $"{Prefix} {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); - - #endregion - - #region ResourceHandle Destructor - - private delegate IntPtr ResourceHandleDestructorDelegate(ResourceHandle* handle); - - [Signature(Sigs.ResourceHandleDestructor, DetourName = nameof(ResourceHandleDestructorDetour))] - private readonly Hook _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( - $"{Prefix} Error in {nameof(ResourceHandleDestructor)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - Penumbra.Log.Excessive($"{Prefix} {nameof(ResourceHandleDestructor)} triggered with 0x{(nint)handle:X}."); - return _resourceHandleDestructorHook!.Original(handle); - } - - public delegate void ResourceHandleDestructorEvent(ResourceHandle* handle); - - #endregion - - #region CharacterBaseCreate - - private delegate nint CharacterBaseCreateDelegate(uint a, nint b, nint c, byte d); - - private readonly Hook _characterBaseCreateHook; - - private nint CharacterBaseCreateDetour(uint a, nint b, nint c, byte d) - { - if (CreatingCharacterBase != null) - foreach (var subscriber in CreatingCharacterBase.GetInvocationList()) - { - try - { - ((CreatingCharacterBaseEvent)subscriber).Invoke((nint)(&a), b, c); - } - catch (Exception ex) - { - Penumbra.Log.Error( - $"{Prefix} Error in {nameof(CharacterBaseCreateDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - var ret = _characterBaseCreateHook.Original(a, b, c, d); - if (CharacterBaseCreated != null) - foreach (var subscriber in CharacterBaseCreated.GetInvocationList()) - { - try - { - ((CharacterBaseCreatedEvent)subscriber).Invoke(a, b, c, ret); - } - catch (Exception ex) - { - Penumbra.Log.Error( - $"{Prefix} Error in {nameof(CharacterBaseCreateDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - return ret; - } - - public delegate void CreatingCharacterBaseEvent(nint modelCharaId, nint customize, nint equipment); - public delegate void CharacterBaseCreatedEvent(uint modelCharaId, nint customize, nint equipment, nint drawObject); - - #endregion - - #region CharacterBase Destructor - - public delegate void CharacterBaseDestructorEvent(nint drawBase); - - private readonly Hook _characterBaseDestructorHook; - - private void CharacterBaseDestructorDetour(IntPtr drawBase) - { - if (CharacterBaseDestructor != null) - foreach (var subscriber in CharacterBaseDestructor.GetInvocationList()) - { - try - { - ((CharacterBaseDestructorEvent)subscriber).Invoke(drawBase); - } - catch (Exception ex) - { - Penumbra.Log.Error( - $"{Prefix} Error in {nameof(CharacterBaseDestructorDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - _characterBaseDestructorHook.Original.Invoke(drawBase); - } - - #endregion - - #region Weapon Reload - - private delegate void WeaponReloadFunc(nint a1, uint a2, nint a3, byte a4, byte a5, byte a6, byte a7); - - private readonly Hook _weaponReloadHook; - - private void WeaponReloadDetour(nint a1, uint a2, nint a3, byte a4, byte a5, byte a6, byte a7) - { - var gameObject = *(nint*)(a1 + 8); - if (WeaponReloading != null) - foreach (var subscriber in WeaponReloading.GetInvocationList()) - { - try - { - ((WeaponReloadingEvent)subscriber).Invoke(a1, gameObject); - } - catch (Exception ex) - { - Penumbra.Log.Error( - $"{Prefix} Error in {nameof(WeaponReloadDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - - _weaponReloadHook.Original(a1, a2, a3, a4, a5, a6, a7); - - if (WeaponReloaded != null) - foreach (var subscriber in WeaponReloaded.GetInvocationList()) - { - try - { - ((WeaponReloadedEvent)subscriber).Invoke(a1, gameObject); - } - catch (Exception ex) - { - Penumbra.Log.Error( - $"{Prefix} Error in {nameof(WeaponReloadDetour)} event when executing {subscriber.Method.Name}:\n{ex}"); - } - } - } - - public delegate void WeaponReloadingEvent(nint drawDataContainer, nint gameObject); - public delegate void WeaponReloadedEvent(nint drawDataContainer, nint gameObject); - - #endregion - - #region Testing - -#if DEBUG - //[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 89 54 24 ?? 57 48 83 EC ?? 48 8B F9", DetourName = nameof(TestDetour))] - private readonly Hook? _testHook = null; - - private delegate void TestDelegate(nint a1, int a2); - - private void TestDetour(nint a1, int a2) - { - Penumbra.Log.Information($"Test: {a1:X} {a2}"); - _testHook!.Original(a1, a2); - } - - private void EnableDebugHook() - => _testHook?.Enable(); - - private void DisposeDebugHook() - => _testHook?.Dispose(); -#else - private void EnableDebugHook() - { } - - private void DisposeDebugHook() - { } -#endif - - #endregion -} diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs index d25a5638..444b9a48 100644 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Classes; using Penumbra.Communication; using Penumbra.GameData; +using Penumbra.Interop.Hooks; using Penumbra.Services; namespace Penumbra.Interop.Services; @@ -32,9 +33,9 @@ public sealed unsafe class SkinFixer : IDisposable private readonly Hook _onRenderMaterialHook; - private readonly GameEventManager _gameEvents; - private readonly CommunicatorService _communicator; - private readonly CharacterUtility _utility; + private readonly ResourceHandleDestructor _resourceHandleDestructor; + private readonly CommunicatorService _communicator; + private readonly CharacterUtility _utility; // MaterialResourceHandle set private readonly ConcurrentSet _moddedSkinShpkMaterials = new(); @@ -50,15 +51,16 @@ public sealed unsafe class SkinFixer : IDisposable public int ModdedSkinShpkCount => _moddedSkinShpkCount; - public SkinFixer(GameEventManager gameEvents, CharacterUtility utility, CommunicatorService communicator, IGameInteropProvider interop) + public SkinFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, CommunicatorService communicator, + IGameInteropProvider interop) { interop.InitializeFromAttributes(this); - _gameEvents = gameEvents; - _utility = utility; - _communicator = communicator; - _onRenderMaterialHook = interop.HookFromAddress(_humanVTable[62], OnRenderHumanMaterial); + _resourceHandleDestructor = resourceHandleDestructor; + _utility = utility; + _communicator = communicator; + _onRenderMaterialHook = interop.HookFromAddress(_humanVTable[62], OnRenderHumanMaterial); _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.SkinFixer); - _gameEvents.ResourceHandleDestructor += OnResourceHandleDestructor; + _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.SkinFixer); _onRenderMaterialHook.Enable(); } @@ -66,7 +68,7 @@ public sealed unsafe class SkinFixer : IDisposable { _onRenderMaterialHook.Dispose(); _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); - _gameEvents.ResourceHandleDestructor -= OnResourceHandleDestructor; + _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); _moddedSkinShpkMaterials.Clear(); _moddedSkinShpkCount = 0; } diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 66101dcd..6c5f9c25 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -2,7 +2,6 @@ using Dalamud.Utility; using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.Services; -using Penumbra.Util; namespace Penumbra.Mods.Manager; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 900d2770..350c20b2 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -80,6 +80,10 @@ public class Penumbra : IDalamudPlugin _services.GetService(); _services.GetService(); // Initialize before Interface. + + foreach (var service in _services.GetServicesImplementing()) + service.Awaiter.Wait(); + SetupInterface(); SetupApi(); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 54f7a16f..b7f3de9d 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -69,6 +69,7 @@ + @@ -92,8 +93,8 @@ - - + + diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index c7efac04..be94a31e 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -9,7 +9,7 @@ public class CommunicatorService : IDisposable, IService { public CommunicatorService(Logger logger) { - EventWrapper.ChangeLogger(logger); + EventWrapperBase.ChangeLogger(logger); } /// diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index 5a1c9f74..8038152e 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -82,8 +82,7 @@ public static class ServiceManagerA .AddDalamudService(pi); private static ServiceManager AddInterop(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() + => services.AddSingleton() .AddSingleton() .AddSingleton(p => { @@ -135,8 +134,7 @@ public static class ServiceManagerA .AddSingleton(); private static ServiceManager AddResolvers(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() + => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 09e22d67..376bbcf7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using Newtonsoft.Json.Linq; using OtterGui; @@ -8,6 +9,7 @@ using OtterGui.Raii; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; +using Penumbra.Interop.Hooks; using Penumbra.Interop.MaterialPreview; using Penumbra.String; using Penumbra.String.Classes; @@ -503,12 +505,12 @@ public partial class ModEditWindow ColorTablePreviewers.Clear(); } - private unsafe void UnbindFromDrawObjectMaterialInstances(nint characterBase) + private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) { for (var i = MaterialPreviewers.Count; i-- > 0;) { var previewer = MaterialPreviewers[i]; - if ((nint)previewer.DrawObject != characterBase) + if (previewer.DrawObject != characterBase) continue; previewer.Dispose(); @@ -518,7 +520,7 @@ public partial class ModEditWindow for (var i = ColorTablePreviewers.Count; i-- > 0;) { var previewer = ColorTablePreviewers[i]; - if ((nint)previewer.DrawObject != characterBase) + if (previewer.DrawObject != characterBase) continue; previewer.Dispose(); @@ -663,7 +665,7 @@ public partial class ModEditWindow UpdateConstants(); } - public MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) + public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) { _edit = edit; Mtrl = file; @@ -673,16 +675,16 @@ public partial class ModEditWindow LoadShpk(FindAssociatedShpk(out _, out _)); if (writable) { - _edit._gameEvents.CharacterBaseDestructor += UnbindFromDrawObjectMaterialInstances; + _edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab); BindToMaterialInstances(); } } - public void Dispose() + public unsafe void Dispose() { UnbindFromMaterialInstances(); if (Writable) - _edit._gameEvents.CharacterBaseDestructor -= UnbindFromDrawObjectMaterialInstances; + _edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); } public bool Valid diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index df379a1c..8a4bd52b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -13,8 +13,8 @@ using Penumbra.Communication; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; +using Penumbra.Interop.Hooks; using Penumbra.Interop.ResourceTree; -using Penumbra.Interop.Services; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Editor; @@ -32,20 +32,20 @@ public partial class ModEditWindow : Window, IDisposable { private const string WindowBaseLabel = "###SubModEdit"; - private readonly PerformanceTracker _performance; - private readonly ModEditor _editor; - private readonly Configuration _config; - private readonly ItemSwapTab _itemSwapTab; - private readonly MetaFileManager _metaFileManager; - private readonly ActiveCollections _activeCollections; - private readonly StainService _stainService; - private readonly ModMergeTab _modMergeTab; - private readonly CommunicatorService _communicator; - private readonly IDragDropManager _dragDropManager; - private readonly GameEventManager _gameEvents; - private readonly IDataManager _gameData; - private readonly IFramework _framework; - private readonly IObjectTable _objects; + private readonly PerformanceTracker _performance; + private readonly ModEditor _editor; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; + private readonly MetaFileManager _metaFileManager; + private readonly ActiveCollections _activeCollections; + private readonly StainService _stainService; + private readonly ModMergeTab _modMergeTab; + private readonly CommunicatorService _communicator; + private readonly IDragDropManager _dragDropManager; + private readonly IDataManager _gameData; + private readonly IFramework _framework; + private readonly IObjectTable _objects; + private readonly CharacterBaseDestructor _characterBaseDestructor; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -565,26 +565,26 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, - CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents, - ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework) + CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, + ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework, CharacterBaseDestructor characterBaseDestructor) : base(WindowBaseLabel) { - _performance = performance; - _itemSwapTab = itemSwapTab; - _gameData = gameData; - _config = config; - _editor = editor; - _metaFileManager = metaFileManager; - _stainService = stainService; - _activeCollections = activeCollections; - _modMergeTab = modMergeTab; - _communicator = communicator; - _dragDropManager = dragDropManager; - _textures = textures; - _fileDialog = fileDialog; - _gameEvents = gameEvents; - _objects = objects; - _framework = framework; + _performance = performance; + _itemSwapTab = itemSwapTab; + _gameData = gameData; + _config = config; + _editor = editor; + _metaFileManager = metaFileManager; + _stainService = stainService; + _activeCollections = activeCollections; + _modMergeTab = modMergeTab; + _communicator = communicator; + _dragDropManager = dragDropManager; + _textures = textures; + _fileDialog = fileDialog; + _objects = objects; + _framework = framework; + _characterBaseDestructor = characterBaseDestructor; _materialTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); @@ -598,12 +598,12 @@ public partial class ModEditWindow : Window, IDisposable _resourceTreeFactory = resourceTreeFactory; _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions); - _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow); + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow); } public void Dispose() { - _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); + _communicator.ModPathChanged.Unsubscribe(OnModPathChange); _editor?.Dispose(); _materialTab.Dispose(); _modelTab.Dispose(); @@ -613,7 +613,7 @@ public partial class ModEditWindow : Window, IDisposable _center.Dispose(); } - private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) { if (type is ModPathChangeType.Reloaded or ModPathChangeType.Moved) ChangeMod(mod); diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 16353828..a0073d05 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -11,6 +11,18 @@ "Unosquare.Swan.Lite": "3.0.0" } }, + "Microsoft.CodeAnalysis.Common": { + "type": "Direct", + "requested": "[4.8.0, )", + "resolved": "4.8.0", + "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "System.Collections.Immutable": "7.0.0", + "System.Reflection.Metadata": "7.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, "Microsoft.Extensions.DependencyInjection": { "type": "Direct", "requested": "[7.0.0, )", @@ -36,6 +48,11 @@ "System.Text.Encoding.CodePages": "5.0.0" } }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "7.0.0", @@ -46,10 +63,23 @@ "resolved": "5.0.0", "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==", + "dependencies": { + "System.Collections.Immutable": "7.0.0" + } + }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==" + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" }, "System.Text.Encoding.CodePages": { "type": "Transitive",