diff --git a/OtterGui b/OtterGui index 197d23ee..22ae2a89 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 197d23eee167c232000f22ef40a7a2bded913b6c +Subproject commit 22ae2a8993ebf3af2313072968a44905a3fcdd2a diff --git a/Penumbra.GameData b/Penumbra.GameData index db421413..ac3fc098 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit db421413a15c48c63eb883dbfc2ac863c579d4c6 +Subproject commit ac3fc0981ac8f503ac91d2419bd28c54f271763e 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/CharacterBaseVTables.cs b/Penumbra/Interop/CharacterBaseVTables.cs new file mode 100644 index 00000000..40b9a588 --- /dev/null +++ b/Penumbra/Interop/CharacterBaseVTables.cs @@ -0,0 +1,24 @@ +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using OtterGui.Services; +using Penumbra.GameData; + +namespace Penumbra.Interop; + +public sealed unsafe class CharacterBaseVTables : IService +{ + [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + public readonly nint* HumanVTable = null!; + + [Signature(Sigs.WeaponVTable, ScanType = ScanType.StaticAddress)] + public readonly nint* WeaponVTable = null!; + + [Signature(Sigs.DemiHumanVTable, ScanType = ScanType.StaticAddress)] + public readonly nint* DemiHumanVTable = null!; + + [Signature(Sigs.MonsterVTable, ScanType = ScanType.StaticAddress)] + public readonly nint* MonsterVTable = null!; + + public CharacterBaseVTables(IGameInteropProvider interop) + => interop.InitializeFromAttributes(this); +} 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/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs new file mode 100644 index 00000000..2fd87f6e --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -0,0 +1,31 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; +using Penumbra.Interop.PathResolving; +using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class CalculateHeight : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public CalculateHeight(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, true); + } + + public delegate ulong Delegate(Character* character); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private ulong Detour(Character* character) + { + var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true); + using var cmp = _metaState.ResolveRspData(collection.ModCollection); + var ret = Task.Result.Original.Invoke(character); + Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}."); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs new file mode 100644 index 00000000..81f6d552 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs @@ -0,0 +1,34 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class ChangeCustomize : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public ChangeCustomize(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Change Customize", Sigs.ChangeCustomize, Detour, true); + } + + public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private bool Detour(Human* human, CustomizeArray* data, byte skipEquipment) + { + _metaState.CustomizeChangeCollection = _collectionResolver.IdentifyCollection((DrawObject*)human, true); + using var cmp = _metaState.ResolveRspData(_metaState.CustomizeChangeCollection.ModCollection); + using var decal1 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, true); + using var decal2 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, false); + var ret = Task.Result.Original.Invoke(human, data, skipEquipment); + Penumbra.Log.Excessive($"[Change Customize] Invoked on {(nint)human:X} with {(nint)data:X}, {skipEquipment} -> {ret}."); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs new file mode 100644 index 00000000..8ffc050f --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs @@ -0,0 +1,35 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class GetEqpIndirect : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public GetEqpIndirect(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Get EQP Indirect", Sigs.GetEqpIndirect, Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject) + { + // Shortcut because this is also called all the time. + // Same thing is checked at the beginning of the original function. + if ((*(byte*)((nint)drawObject + Offsets.GetEqpIndirectSkip1) & 1) == 0 || *(ulong*)((nint)drawObject + Offsets.GetEqpIndirectSkip2) == 0) + return; + + Penumbra.Log.Excessive($"[Get EQP Indirect] Invoked on {(nint)drawObject:X}."); + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + using var eqp = _metaState.ResolveEqpData(collection.ModCollection); + Task.Result.Original(drawObject); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs new file mode 100644 index 00000000..9f191fdd --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -0,0 +1,30 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class ModelLoadComplete : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public ModelLoadComplete(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState, CharacterBaseVTables vtables) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Model Load Complete", vtables.HumanVTable[58], Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject) + { + Penumbra.Log.Excessive($"[Model Load Complete] Invoked on {(nint)drawObject:X}."); + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + using var eqp = _metaState.ResolveEqpData(collection.ModCollection); + using var eqdp = _metaState.ResolveEqdpData(collection.ModCollection, MetaState.GetDrawObjectGenderRace((nint)drawObject), true, true); + Task.Result.Original(drawObject); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs new file mode 100644 index 00000000..8f8f1d78 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs @@ -0,0 +1,37 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class RspSetupCharacter : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public RspSetupCharacter(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("RSP Setup Character", Sigs.RspSetupCharacter, Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject, nint unk2, float unk3, nint unk4, byte unk5); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject, nint unk2, float unk3, nint unk4, byte unk5) + { + Penumbra.Log.Excessive($"[RSP Setup Character] Invoked on {(nint)drawObject:X} with {unk2}, {unk3}, {unk4}, {unk5}."); + // Skip if we are coming from ChangeCustomize. + if (_metaState.CustomizeChangeCollection.Valid) + { + Task.Result.Original.Invoke(drawObject, unk2, unk3, unk4, unk5); + return; + } + + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + using var cmp = _metaState.ResolveRspData(collection.ModCollection); + Task.Result.Original.Invoke(drawObject, unk2, unk3, unk4, unk5); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs new file mode 100644 index 00000000..e451f118 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs @@ -0,0 +1,35 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +/// +/// GMP. This gets called every time when changing visor state, and it accesses the gmp file itself, +/// but it only applies a changed gmp file after a redraw for some reason. +/// +public sealed unsafe class SetupVisor : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public SetupVisor(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Setup Visor", Sigs.SetupVisor, Detour, true); + } + + public delegate byte Delegate(DrawObject* drawObject, ushort modelId, byte visorState); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private byte Detour(DrawObject* drawObject, ushort modelId, byte visorState) + { + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + using var gmp = _metaState.ResolveGmpData(collection.ModCollection); + var ret = Task.Result.Original.Invoke(drawObject, modelId, visorState); + Penumbra.Log.Excessive($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}."); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs new file mode 100644 index 00000000..786ad5f2 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs @@ -0,0 +1,36 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class UpdateModel : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public UpdateModel(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Update Model", Sigs.UpdateModel, Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject) + { + // Shortcut because this is called all the time. + // Same thing is checked at the beginning of the original function. + if (*(int*)((nint)drawObject + Offsets.UpdateModelSkip) == 0) + return; + + Penumbra.Log.Excessive($"[Update Model] Invoked on {(nint)drawObject:X}."); + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + using var eqp = _metaState.ResolveEqpData(collection.ModCollection); + using var eqdp = _metaState.ResolveEqdpData(collection.ModCollection, MetaState.GetDrawObjectGenderRace((nint)drawObject), true, true); + Task.Result.Original.Invoke(drawObject); + } +} 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..1a715f13 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. + /// 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..b944011d 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..9d899648 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -1,22 +1,16 @@ -using Dalamud.Hooking; -using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; 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; using Penumbra.String.Classes; -using Penumbra.Util; using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; -using static Penumbra.GameData.Enums.GenderRace; namespace Penumbra.Interop.PathResolving; @@ -42,56 +36,40 @@ namespace Penumbra.Interop.PathResolving; // ChangeCustomize and RspSetupCharacter, which is hooked here, as well as Character.CalculateHeight. // GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter. -public unsafe class MetaState : IDisposable +public sealed unsafe class MetaState : IDisposable { - [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] - private readonly nint* _humanVTable = null!; - private readonly Configuration _config; private readonly CommunicatorService _communicator; - private readonly PerformanceTracker _performance; private readonly CollectionResolver _collectionResolver; private readonly ResourceLoader _resources; - private readonly GameEventManager _gameEventManager; private readonly CharacterUtility _characterUtility; + private readonly CreateCharacterBase _createCharacterBase; + + public ResolveData CustomizeChangeCollection = ResolveData.Invalid; 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, - IGameInteropProvider interop) + public MetaState(CommunicatorService communicator, CollectionResolver collectionResolver, + ResourceLoader resources, CreateCharacterBase createCharacterBase, CharacterUtility characterUtility, Configuration config) { - _performance = performance; - _communicator = communicator; - _collectionResolver = collectionResolver; - _resources = resources; - _gameEventManager = gameEventManager; - _characterUtility = characterUtility; - _config = config; - interop.InitializeFromAttributes(this); - _calculateHeightHook = - interop.HookFromAddress((nint)Character.MemberFunctionPointers.CalculateHeight, CalculateHeightDetour); - _onModelLoadCompleteHook = interop.HookFromAddress(_humanVTable[58], OnModelLoadCompleteDetour); - _getEqpIndirectHook.Enable(); - _updateModelsHook.Enable(); - _onModelLoadCompleteHook.Enable(); - _setupVisorHook.Enable(); - _rspSetupCharacterHook.Enable(); - _changeCustomize.Enable(); - _calculateHeightHook.Enable(); - _gameEventManager.CreatingCharacterBase += OnCreatingCharacterBase; - _gameEventManager.CharacterBaseCreated += OnCharacterBaseCreated; + _communicator = communicator; + _collectionResolver = collectionResolver; + _resources = resources; + _createCharacterBase = createCharacterBase; + _characterUtility = characterUtility; + _config = config; + _createCharacterBase.Subscribe(OnCreatingCharacterBase, CreateCharacterBase.Priority.MetaState); + _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.MetaState); } public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData) { if (type == ResourceType.Tex - && (_lastCreatedCollection.Valid || _customizeChangeCollection.Valid) + && (_lastCreatedCollection.Valid || CustomizeChangeCollection.Valid) && gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8)) { - resolveData = _lastCreatedCollection.Valid ? _lastCreatedCollection : _customizeChangeCollection; + resolveData = _lastCreatedCollection.Valid ? _lastCreatedCollection : CustomizeChangeCollection; return true; } @@ -100,186 +78,81 @@ public unsafe class MetaState : IDisposable } public DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory) - { - var races = race.Dependencies(); + => (equipment, accessory) switch + { + (true, true) => new DisposableContainer(race.Dependencies().SelectMany(r => new[] + { + collection.TemporarilySetEqdpFile(_characterUtility, r, false), + collection.TemporarilySetEqdpFile(_characterUtility, r, true), + })), + (true, false) => new DisposableContainer(race.Dependencies() + .Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false))), + (false, true) => new DisposableContainer(race.Dependencies() + .Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, true))), + _ => DisposableContainer.Empty, + }; - var equipmentEnumerable = equipment - ? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false)) - : Array.Empty().AsEnumerable(); - var accessoryEnumerable = accessory - ? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, true)) - : Array.Empty().AsEnumerable(); - return new DisposableContainer(equipmentEnumerable.Concat(accessoryEnumerable)); - } + public MetaList.MetaReverter ResolveEqpData(ModCollection collection) + => collection.TemporarilySetEqpFile(_characterUtility); + + public MetaList.MetaReverter ResolveGmpData(ModCollection collection) + => collection.TemporarilySetGmpFile(_characterUtility); + + public MetaList.MetaReverter ResolveRspData(ModCollection collection) + => collection.TemporarilySetCmpFile(_characterUtility); + + public DecalReverter ResolveDecal(ResolveData resolve, bool which) + => new(_config, _characterUtility, _resources, resolve, which); public static GenderRace GetHumanGenderRace(nint human) => (GenderRace)((Human*)human)->RaceSexId; - public void Dispose() + public static GenderRace GetDrawObjectGenderRace(nint drawObject) { - _getEqpIndirectHook.Dispose(); - _updateModelsHook.Dispose(); - _onModelLoadCompleteHook.Dispose(); - _setupVisorHook.Dispose(); - _rspSetupCharacterHook.Dispose(); - _changeCustomize.Dispose(); - _calculateHeightHook.Dispose(); - _gameEventManager.CreatingCharacterBase -= OnCreatingCharacterBase; - _gameEventManager.CharacterBaseCreated -= OnCharacterBaseCreated; + var draw = (DrawObject*)drawObject; + if (draw->Object.GetObjectType() != ObjectType.CharacterBase) + return GenderRace.Unknown; + + var c = (CharacterBase*)drawObject; + return c->GetModelType() == CharacterBase.ModelType.Human + ? GetHumanGenderRace(drawObject) + : GenderRace.Unknown; } - private void OnCreatingCharacterBase(nint modelCharaId, nint customize, nint equipData) + public void Dispose() + { + _createCharacterBase.Unsubscribe(OnCreatingCharacterBase); + _createCharacterBase.Unsubscribe(OnCharacterBaseCreated); + } + + 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; } - private delegate void OnModelLoadCompleteDelegate(nint drawObject); - private readonly Hook _onModelLoadCompleteHook; - - private void OnModelLoadCompleteDetour(nint drawObject) - { - var collection = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var eqp = collection.ModCollection.TemporarilySetEqpFile(_characterUtility); - using var eqdp = ResolveEqdpData(collection.ModCollection, GetDrawObjectGenderRace(drawObject), true, true); - _onModelLoadCompleteHook.Original.Invoke(drawObject); - } - - private delegate void UpdateModelDelegate(nint drawObject); - - [Signature(Sigs.UpdateModel, DetourName = nameof(UpdateModelsDetour))] - private readonly Hook _updateModelsHook = null!; - - private void UpdateModelsDetour(nint drawObject) - { - // Shortcut because this is called all the time. - // Same thing is checked at the beginning of the original function. - if (*(int*)(drawObject + Offsets.UpdateModelSkip) == 0) - return; - - using var performance = _performance.Measure(PerformanceType.UpdateModels); - - var collection = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var eqp = collection.ModCollection.TemporarilySetEqpFile(_characterUtility); - using var eqdp = ResolveEqdpData(collection.ModCollection, GetDrawObjectGenderRace(drawObject), true, true); - _updateModelsHook.Original.Invoke(drawObject); - } - - private static GenderRace GetDrawObjectGenderRace(nint drawObject) - { - var draw = (DrawObject*)drawObject; - if (draw->Object.GetObjectType() != ObjectType.CharacterBase) - return Unknown; - - var c = (CharacterBase*)drawObject; - return c->GetModelType() == CharacterBase.ModelType.Human - ? GetHumanGenderRace(drawObject) - : Unknown; - } - - [Signature(Sigs.GetEqpIndirect, DetourName = nameof(GetEqpIndirectDetour))] - private readonly Hook _getEqpIndirectHook = null!; - - private void GetEqpIndirectDetour(nint drawObject) - { - // Shortcut because this is also called all the time. - // Same thing is checked at the beginning of the original function. - if ((*(byte*)(drawObject + Offsets.GetEqpIndirectSkip1) & 1) == 0 || *(ulong*)(drawObject + Offsets.GetEqpIndirectSkip2) == 0) - return; - - using var performance = _performance.Measure(PerformanceType.GetEqp); - var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var eqp = resolveData.ModCollection.TemporarilySetEqpFile(_characterUtility); - _getEqpIndirectHook.Original(drawObject); - } - - - // GMP. This gets called every time when changing visor state, and it accesses the gmp file itself, - // but it only applies a changed gmp file after a redraw for some reason. - private delegate byte SetupVisorDelegate(nint drawObject, ushort modelId, byte visorState); - - [Signature(Sigs.SetupVisor, DetourName = nameof(SetupVisorDetour))] - private readonly Hook _setupVisorHook = null!; - - private byte SetupVisorDetour(nint drawObject, ushort modelId, byte visorState) - { - using var performance = _performance.Measure(PerformanceType.SetupVisor); - var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var gmp = resolveData.ModCollection.TemporarilySetGmpFile(_characterUtility); - return _setupVisorHook.Original(drawObject, modelId, visorState); - } - - // RSP - private delegate void RspSetupCharacterDelegate(nint drawObject, nint unk2, float unk3, nint unk4, byte unk5); - - [Signature(Sigs.RspSetupCharacter, DetourName = nameof(RspSetupCharacterDetour))] - private readonly Hook _rspSetupCharacterHook = null!; - - private void RspSetupCharacterDetour(nint drawObject, nint unk2, float unk3, nint unk4, byte unk5) - { - if (_customizeChangeCollection.Valid) - { - _rspSetupCharacterHook.Original(drawObject, unk2, unk3, unk4, unk5); - } - else - { - using var performance = _performance.Measure(PerformanceType.SetupCharacter); - var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(_characterUtility); - _rspSetupCharacterHook.Original(drawObject, unk2, unk3, unk4, unk5); - } - } - - private delegate ulong CalculateHeightDelegate(Character* character); - - private readonly Hook _calculateHeightHook = null!; - - private ulong CalculateHeightDetour(Character* character) - { - var resolveData = _collectionResolver.IdentifyCollection((GameObject*)character, true); - using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(_characterUtility); - return _calculateHeightHook.Original(character); - } - - private delegate bool ChangeCustomizeDelegate(nint human, nint data, byte skipEquipment); - - [Signature(Sigs.ChangeCustomize, DetourName = nameof(ChangeCustomizeDetour))] - private readonly Hook _changeCustomize = null!; - - private bool ChangeCustomizeDetour(nint human, nint data, byte skipEquipment) - { - using var performance = _performance.Measure(PerformanceType.ChangeCustomize); - _customizeChangeCollection = _collectionResolver.IdentifyCollection((DrawObject*)human, true); - using var cmp = _customizeChangeCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); - using var decals = new DecalReverter(_config, _characterUtility, _resources, _customizeChangeCollection, true); - using var decal2 = new DecalReverter(_config, _characterUtility, _resources, _customizeChangeCollection, false); - var ret = _changeCustomize.Original(human, data, skipEquipment); - _customizeChangeCollection = ResolveData.Invalid; - return ret; - } - /// /// Check the customize array for the FaceCustomization byte and the last bit of that. /// Also check for humans. /// - public static bool UsesDecal(uint modelId, nint customizeData) + private static bool UsesDecal(uint modelId, nint customizeData) => modelId == 0 && ((byte*)customizeData)[12] > 0x7F; } 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 e808d975..f1956742 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -69,6 +69,7 @@ + @@ -94,8 +95,8 @@ - - + + diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index e8684f9d..a542dab5 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -1,15 +1,24 @@ using Newtonsoft.Json.Linq; using OtterGui.Classes; using OtterGui.Log; +using OtterGui.Services; namespace Penumbra.Services; -public class BackupService +public class BackupService : IAsyncService { + /// + public Task Awaiter { get; } + + /// + public bool Finished + => Awaiter.IsCompletedSuccessfully; + + /// Start a backup process on the collected files. public BackupService(Logger logger, FilenameService fileNames) { - var files = PenumbraFiles(fileNames); - Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files); + var files = PenumbraFiles(fileNames); + Awaiter = Task.Run(() => Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files)); } /// Collect all relevant files for penumbra configuration. diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 3e61e3c1..be94a31e 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -1,14 +1,15 @@ using OtterGui.Classes; using OtterGui.Log; +using OtterGui.Services; using Penumbra.Communication; namespace Penumbra.Services; -public class CommunicatorService : IDisposable +public class CommunicatorService : IDisposable, IService { public CommunicatorService(Logger logger) { - EventWrapper.ChangeLogger(logger); + EventWrapperBase.ChangeLogger(logger); } /// diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index beb23fa2..b84c0996 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -22,10 +22,8 @@ namespace Penumbra.Services; /// Contains everything to migrate from older versions of the config to the current, /// including deprecated fields. /// -public class ConfigMigrationService +public class ConfigMigrationService(SaveService saveService) : IService { - private readonly SaveService _saveService; - private Configuration _config = null!; private JObject _data = null!; @@ -33,14 +31,11 @@ public class ConfigMigrationService public string DefaultCollection = ModCollection.DefaultCollectionName; public string ForcedCollection = string.Empty; public Dictionary CharacterCollections = []; - public Dictionary ModSortOrder = new(); + public Dictionary ModSortOrder = []; public bool InvertModListOrder; public bool SortFoldersFirst; public SortModeV3 SortMode = SortModeV3.FoldersFirst; - public ConfigMigrationService(SaveService saveService) - => _saveService = saveService; - /// Add missing colors to the dictionary if necessary. private static void AddColors(Configuration config, bool forceSave) { @@ -61,13 +56,13 @@ public class ConfigMigrationService // because it stayed alive for a bunch of people for some reason. DeleteMetaTmp(); - if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(_saveService.FileNames.ConfigFile)) + if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(saveService.FileNames.ConfigFile)) { AddColors(config, false); return; } - _data = JObject.Parse(File.ReadAllText(_saveService.FileNames.ConfigFile)); + _data = JObject.Parse(File.ReadAllText(saveService.FileNames.ConfigFile)); CreateBackup(); Version0To1(); @@ -118,7 +113,7 @@ public class ConfigMigrationService if (_config.Version != 6) return; - ActiveCollectionMigration.MigrateUngenderedCollections(_saveService.FileNames); + ActiveCollectionMigration.MigrateUngenderedCollections(saveService.FileNames); _config.Version = 7; } @@ -223,7 +218,7 @@ public class ConfigMigrationService return; // Add the previous forced collection to all current collections except itself as an inheritance. - foreach (var collection in _saveService.FileNames.CollectionFiles) + foreach (var collection in saveService.FileNames.CollectionFiles) { try { @@ -246,7 +241,7 @@ public class ConfigMigrationService private void ResettleSortOrder() { ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject>() ?? ModSortOrder; - var file = _saveService.FileNames.FilesystemFile; + var file = saveService.FileNames.FilesystemFile; using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); using var writer = new StreamWriter(stream); using var j = new JsonTextWriter(writer); @@ -281,7 +276,7 @@ public class ConfigMigrationService private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters, IEnumerable<(CollectionType, string)> special) { - var file = _saveService.FileNames.ActiveCollectionsFile; + var file = saveService.FileNames.ActiveCollectionsFile; try { using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); @@ -337,7 +332,7 @@ public class ConfigMigrationService if (!collectionJson.Exists) return; - var defaultCollectionFile = new FileInfo(_saveService.FileNames.CollectionFile(ModCollection.DefaultCollectionName)); + var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollection.DefaultCollectionName)); if (defaultCollectionFile.Exists) return; @@ -370,9 +365,9 @@ public class ConfigMigrationService dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); var emptyStorage = new ModStorage(); - var collection = ModCollection.CreateFromData(_saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, + var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, Array.Empty()); - _saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); + saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) { @@ -384,7 +379,7 @@ public class ConfigMigrationService // Create a backup of the configuration file specifically. private void CreateBackup() { - var name = _saveService.FileNames.ConfigFile; + var name = saveService.FileNames.ConfigFile; var bakName = name + ".bak"; try { diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudConfigService.cs similarity index 72% rename from Penumbra/Services/DalamudServices.cs rename to Penumbra/Services/DalamudConfigService.cs index 51fb1192..8379a3e7 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudConfigService.cs @@ -1,21 +1,12 @@ -using Dalamud.Game; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Interface; -using Dalamud.IoC; using Dalamud.Plugin; -using Dalamud.Interface.DragDrop; -using Dalamud.Plugin.Services; using OtterGui.Services; -// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local - namespace Penumbra.Services; -public class DalamudConfigService +public class DalamudConfigService : IService { - public DalamudConfigService(DalamudPluginInterface pluginInterface) + public DalamudConfigService() { - pluginInterface.Inject(this); try { var serviceType = @@ -115,29 +106,3 @@ public class DalamudConfigService } } } - -public static class DalamudServices -{ - public static void AddServices(ServiceManager services, DalamudPluginInterface pi) - { - services.AddExistingService(pi); - services.AddExistingService(pi.UiBuilder); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - services.AddDalamudService(pi); - } -} diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 52881b9e..5f918a90 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -1,11 +1,11 @@ using Dalamud.Plugin; -using OtterGui.Filesystem; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Mods; namespace Penumbra.Services; -public class FilenameService(DalamudPluginInterface pi) +public class FilenameService(DalamudPluginInterface pi) : IService { public readonly string ConfigDirectory = pi.ConfigDirectory.FullName; public readonly string CollectionDirectory = Path.Combine(pi.ConfigDirectory.FullName, "collections"); diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index c893b00f..06c3c4d0 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -5,15 +5,12 @@ using Dalamud.Interface; using Dalamud.Plugin.Services; using Lumina.Excel.GeneratedSheets; using OtterGui.Log; +using OtterGui.Services; namespace Penumbra.Services; -public class MessageService : OtterGui.Classes.MessageService +public class MessageService(Logger log, UiBuilder uiBuilder, IChatGui chat) : OtterGui.Classes.MessageService(log, uiBuilder, chat), IService { - public MessageService(Logger log, UiBuilder uiBuilder, IChatGui chat) - : base(log, uiBuilder, chat) - { } - public void LinkItem(Item item) { // @formatter:off diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 40dc4107..801e0c1d 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using OtterGui.Log; +using OtterGui.Services; using Penumbra.Mods; using Penumbra.Mods.Subclasses; @@ -11,12 +12,9 @@ namespace Penumbra.Services; public interface ISavable : ISavable { } -public sealed class SaveService : SaveServiceBase +public sealed class SaveService(Logger log, FrameworkManager framework, FilenameService fileNames, BackupService backupService) + : SaveServiceBase(log, framework, fileNames, backupService.Awaiter), IService { - public SaveService(Logger log, FrameworkManager framework, FilenameService fileNames) - : base(log, framework, fileNames) - { } - /// Immediately delete all existing option group files for a mod and save them anew. public void SaveAllOptionGroups(Mod mod, bool backup, bool onlyAscii) { diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index c04c35c7..a81ae55b 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -1,5 +1,10 @@ +using Dalamud.Game; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Interface.DragDrop; using Dalamud.Plugin; +using Dalamud.Plugin.Services; using Microsoft.Extensions.DependencyInjection; +using OtterGui; using OtterGui.Classes; using OtterGui.Compression; using OtterGui.Log; @@ -8,7 +13,6 @@ using Penumbra.Api; using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; -using Penumbra.GameData.Data; using Penumbra.Import.Models; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Structs; @@ -38,10 +42,9 @@ public static class ServiceManagerA public static ServiceManager CreateProvider(Penumbra penumbra, DalamudPluginInterface pi, Logger log) { var services = new ServiceManager(log) + .AddDalamudServices(pi) .AddExistingService(log) .AddExistingService(penumbra) - .AddMeta() - .AddGameData() .AddInterop() .AddConfiguration() .AddCollections() @@ -53,31 +56,34 @@ public static class ServiceManagerA .AddApi(); services.AddIServices(typeof(EquipItem).Assembly); services.AddIServices(typeof(Penumbra).Assembly); - DalamudServices.AddServices(services, pi); + services.AddIServices(typeof(ImGuiUtil).Assembly); services.CreateProvider(); return services; } - private static ServiceManager AddMeta(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - - private static ServiceManager AddGameData(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton(); + private static ServiceManager AddDalamudServices(this ServiceManager services, DalamudPluginInterface pi) + => services.AddExistingService(pi) + .AddExistingService(pi.UiBuilder) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi) + .AddDalamudService(pi); private static ServiceManager AddInterop(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() + => services.AddSingleton() .AddSingleton() .AddSingleton(p => { @@ -96,8 +102,7 @@ public static class ServiceManagerA .AddSingleton(); private static ServiceManager AddConfiguration(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() + => services.AddSingleton() .AddSingleton(); private static ServiceManager AddCollections(this ServiceManager services) @@ -130,8 +135,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/Services/StainService.cs b/Penumbra/Services/StainService.cs index 714576b2..00fc0737 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -1,36 +1,26 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using Dalamud.Plugin; using Dalamud.Plugin.Services; using ImGuiNET; -using OtterGui.Log; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Files; using Penumbra.UI.AdvancedWindow; -using Penumbra.Util; namespace Penumbra.Services; -public class StainService : IDisposable +public class StainService : IService { - public sealed class StainTemplateCombo : FilterComboCache + public sealed class StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile) + : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), Penumbra.Log) { - private readonly StmFile _stmFile; - private readonly FilterComboColors _stainCombo; - - public StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile) - : base(stmFile.Entries.Keys.Prepend((ushort)0), Penumbra.Log) - { - _stainCombo = stainCombo; - _stmFile = stmFile; - } - protected override float GetFilterWidth() { - var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X; - if (_stainCombo.CurrentSelection.Key == 0) + var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X; + if (stainCombo.CurrentSelection.Key == 0) return baseSize; + return baseSize + ImGui.GetTextLineHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 3; } @@ -46,19 +36,19 @@ public class StainService : IDisposable public override bool Draw(string label, string preview, string tooltip, ref int currentSelection, float previewWidth, float itemHeight, ImGuiComboFlags flags = ImGuiComboFlags.None) { - using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(1, 0.5f)) .Push(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X }); var spaceSize = ImGui.CalcTextSize(" ").X; - var spaces = (int) (previewWidth / spaceSize) - 1; + var spaces = (int)(previewWidth / spaceSize) - 1; return base.Draw(label, preview.PadLeft(spaces), tooltip, ref currentSelection, previewWidth, itemHeight, flags); } protected override bool DrawSelectable(int globalIdx, bool selected) { var ret = base.DrawSelectable(globalIdx, selected); - var selection = _stainCombo.CurrentSelection.Key; - if (selection == 0 || !_stmFile.TryGetValue(Items[globalIdx], selection, out var colors)) + var selection = stainCombo.CurrentSelection.Key; + if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors)) return ret; ImGui.SameLine(); @@ -72,25 +62,18 @@ public class StainService : IDisposable } } - public readonly DictStains StainData; + public readonly DictStain StainData; public readonly FilterComboColors StainCombo; public readonly StmFile StmFile; public readonly StainTemplateCombo TemplateCombo; - public StainService(DalamudPluginInterface pluginInterface, IDataManager dataManager, Logger logger) + public StainService(IDataManager dataManager, DictStain stainData) { - StainData = new DictStains(pluginInterface, logger, dataManager); + StainData = stainData; StainCombo = new FilterComboColors(140, () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), Penumbra.Log); StmFile = new StmFile(dataManager); TemplateCombo = new StainTemplateCombo(StainCombo, StmFile); - Penumbra.Log.Verbose($"[{nameof(StainService)}] Created."); - } - - public void Dispose() - { - StainData.Dispose(); - Penumbra.Log.Verbose($"[{nameof(StainService)}] Disposed."); } } diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index 7287938c..4d071f85 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -1,10 +1,11 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin; using OtterGui.Classes; +using OtterGui.Services; namespace Penumbra.Services; -public class ValidityChecker +public class ValidityChecker : IService { public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json"; public const string SeaOfStars = "https://raw.githubusercontent.com/Ottermandias/SeaOfStars/main/repo.json"; 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 1576a157..96957ba8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -14,8 +14,8 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Models; 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; @@ -33,20 +33,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; @@ -566,27 +566,27 @@ 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, ModelManager models, IDragDropManager dragDropManager, GameEventManager gameEvents, - ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework) + CommunicatorService communicator, TextureManager textures, ModelManager models, 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; + _performance = performance; + _itemSwapTab = itemSwapTab; + _gameData = gameData; + _config = config; + _editor = editor; + _metaFileManager = metaFileManager; + _stainService = stainService; + _activeCollections = activeCollections; + _modMergeTab = modMergeTab; + _communicator = communicator; + _dragDropManager = dragDropManager; + _textures = textures; _models = models; - _fileDialog = fileDialog; - _gameEvents = gameEvents; - _objects = objects; - _framework = framework; + _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)); @@ -600,12 +600,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(); @@ -615,7 +615,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/Util/PerformanceType.cs b/Penumbra/Util/PerformanceType.cs index b84813cc..d5755dfd 100644 --- a/Penumbra/Util/PerformanceType.cs +++ b/Penumbra/Util/PerformanceType.cs @@ -1,7 +1,10 @@ -global using PerformanceTracker = OtterGui.Classes.PerformanceTracker; +using Dalamud.Plugin.Services; +using OtterGui.Services; namespace Penumbra.Util; +public sealed class PerformanceTracker(IFramework framework) : OtterGui.Classes.PerformanceTracker(framework), IService; + public enum PerformanceType { UiMainWindow, diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 7017bb08..cb873592 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, )", @@ -51,6 +63,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", @@ -69,10 +86,23 @@ "SharpGLTF.Core": "1.0.0-alpha0030" } }, + "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",