Move all animation and game event hooks to own classes.

This commit is contained in:
Ottermandias 2023-12-31 15:10:30 +01:00
parent 81cdcad72e
commit da019e729d
62 changed files with 1402 additions and 1143 deletions

@ -1 +1 @@
Subproject commit f6a8ad0f8e585408e0aa17c90209358403b52535
Subproject commit 22ae2a8993ebf3af2313072968a44905a3fcdd2a

View file

@ -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;

View file

@ -11,7 +11,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is the clicked object data if any. </item>
/// </list>
/// </summary>
public sealed class ChangedItemClick : EventWrapper<Action<MouseButton, object?>, ChangedItemClick.Priority>
public sealed class ChangedItemClick() : EventWrapper<MouseButton, object?, ChangedItemClick.Priority>(nameof(ChangedItemClick))
{
public enum Priority
{
@ -21,11 +21,4 @@ public sealed class ChangedItemClick : EventWrapper<Action<MouseButton, object?>
/// <seealso cref="Penumbra.SetupApi"/>
Link = 1,
}
public ChangedItemClick()
: base(nameof(ChangedItemClick))
{ }
public void Invoke(MouseButton button, object? data)
=> Invoke(this, button, data);
}

View file

@ -8,7 +8,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is the hovered object data if any. </item>
/// </list>
/// </summary>
public sealed class ChangedItemHover : EventWrapper<Action<object?>, ChangedItemHover.Priority>
public sealed class ChangedItemHover() : EventWrapper<object?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
{
public enum Priority
{
@ -19,13 +19,6 @@ public sealed class ChangedItemHover : EventWrapper<Action<object?>, ChangedItem
Link = 1,
}
public ChangedItemHover()
: base(nameof(ChangedItemHover))
{ }
public void Invoke(object? data)
=> Invoke(this, data);
public bool HasTooltip
=> HasSubscribers;
}

View file

@ -12,7 +12,8 @@ namespace Penumbra.Communication;
/// <item>Parameter is the new collection, or null on deletions.</item>
/// <item>Parameter is the display name for Individual collections or an empty string otherwise.</item>
/// </list> </summary>
public sealed class CollectionChange : EventWrapper<Action<CollectionType, ModCollection?, ModCollection?, string>, CollectionChange.Priority>
public sealed class CollectionChange()
: EventWrapper<CollectionType, ModCollection?, ModCollection?, string, CollectionChange.Priority>(nameof(CollectionChange))
{
public enum Priority
{
@ -46,11 +47,4 @@ public sealed class CollectionChange : EventWrapper<Action<CollectionType, ModCo
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnCollectionChange"/>
ModFileSystemSelector = 0,
}
public CollectionChange()
: base(nameof(CollectionChange))
{ }
public void Invoke(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string displayName)
=> Invoke(this, collectionType, oldCollection, newCollection, displayName);
}

View file

@ -10,7 +10,8 @@ namespace Penumbra.Communication;
/// <item>Parameter is whether the change was itself inherited, i.e. if it happened in a direct parent (false) or a more removed ancestor (true). </item>
/// </list>
/// </summary>
public sealed class CollectionInheritanceChanged : EventWrapper<Action<ModCollection, bool>, CollectionInheritanceChanged.Priority>
public sealed class CollectionInheritanceChanged()
: EventWrapper<ModCollection, bool, CollectionInheritanceChanged.Priority>(nameof(CollectionInheritanceChanged))
{
public enum Priority
{
@ -23,11 +24,4 @@ public sealed class CollectionInheritanceChanged : EventWrapper<Action<ModCollec
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnInheritanceChange"/>
ModFileSystemSelector = 0,
}
public CollectionInheritanceChanged()
: base(nameof(CollectionInheritanceChanged))
{ }
public void Invoke(ModCollection collection, bool inherited)
=> Invoke(this, collection, inherited);
}

View file

@ -9,18 +9,12 @@ namespace Penumbra.Communication;
/// <item>Parameter is the applied collection. </item>
/// <item>Parameter is the created draw object. </item>
/// </list> </summary>
public sealed class CreatedCharacterBase : EventWrapper<Action<nint, ModCollection, nint>, CreatedCharacterBase.Priority>
public sealed class CreatedCharacterBase()
: EventWrapper<nint, ModCollection, nint, CreatedCharacterBase.Priority>(nameof(CreatedCharacterBase))
{
public enum Priority
{
/// <seealso cref="PenumbraApi.CreatedCharacterBase"/>
Api = int.MinValue,
}
public CreatedCharacterBase()
: base(nameof(CreatedCharacterBase))
{ }
public void Invoke(nint gameObject, ModCollection appliedCollection, nint drawObject)
=> Invoke(this, gameObject, appliedCollection, drawObject);
}

View file

@ -12,18 +12,12 @@ namespace Penumbra.Communication;
/// <item>Parameter is a pointer to the customize array. </item>
/// <item>Parameter is a pointer to the equip data array. </item>
/// </list> </summary>
public sealed class CreatingCharacterBase : EventWrapper<Action<nint, string, nint, nint, nint>, CreatingCharacterBase.Priority>
public sealed class CreatingCharacterBase()
: EventWrapper<nint, string, nint, nint, nint, CreatingCharacterBase.Priority>(nameof(CreatingCharacterBase))
{
public enum Priority
{
/// <seealso cref="PenumbraApi.CreatingCharacterBase"/>
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);
}

View file

@ -9,7 +9,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is whether Penumbra is now Enabled (true) or Disabled (false). </item>
/// </list>
/// </summary>
public sealed class EnabledChanged : EventWrapper<Action<bool>, EnabledChanged.Priority>
public sealed class EnabledChanged() : EventWrapper<bool, EnabledChanged.Priority>(nameof(EnabledChanged))
{
public enum Priority
{
@ -19,11 +19,4 @@ public sealed class EnabledChanged : EventWrapper<Action<bool>, EnabledChanged.P
/// <seealso cref="Api.DalamudSubstitutionProvider.OnEnabledChange"/>
DalamudSubstitutionProvider = 0,
}
public EnabledChanged()
: base(nameof(EnabledChanged))
{ }
public void Invoke(bool enabled)
=> Invoke(this, enabled);
}

View file

@ -11,7 +11,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the old name of the mod in case of a name change, and null otherwise. </item>
/// </list> </summary>
public sealed class ModDataChanged : EventWrapper<Action<ModDataChangeType, Mod, string?>, ModDataChanged.Priority>
public sealed class ModDataChanged() : EventWrapper<ModDataChangeType, Mod, string?, ModDataChanged.Priority>(nameof(ModDataChanged))
{
public enum Priority
{
@ -27,11 +27,4 @@ public sealed class ModDataChanged : EventWrapper<Action<ModDataChangeType, Mod,
/// <seealso cref="UI.ModsTab.ModPanelHeader.OnModDataChange"/>
ModPanelHeader = 0,
}
public ModDataChanged()
: base(nameof(ModDataChanged))
{ }
public void Invoke(ModDataChangeType changeType, Mod mod, string? oldName)
=> Invoke(this, changeType, mod, oldName);
}

View file

@ -10,7 +10,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is whether the new directory is valid. </item>
/// </list>
/// </summary>
public sealed class ModDirectoryChanged : EventWrapper<Action<string, bool>, ModDirectoryChanged.Priority>
public sealed class ModDirectoryChanged() : EventWrapper<string, bool, ModDirectoryChanged.Priority>(nameof(ModDirectoryChanged))
{
public enum Priority
{
@ -20,11 +20,4 @@ public sealed class ModDirectoryChanged : EventWrapper<Action<string, bool>, Mod
/// <seealso cref="UI.FileDialogService.OnModDirectoryChange"/>
FileDialogService = 0,
}
public ModDirectoryChanged()
: base(nameof(ModDirectoryChanged))
{ }
public void Invoke(string newModDirectory, bool newDirectoryValid)
=> Invoke(this, newModDirectory, newDirectoryValid);
}

View file

@ -1,10 +1,9 @@
using OtterGui.Classes;
using Penumbra.Mods.Manager;
namespace Penumbra.Communication;
/// <summary> Triggered whenever a new mod discovery has finished. </summary>
public sealed class ModDiscoveryFinished : EventWrapper<Action, ModDiscoveryFinished.Priority>
public sealed class ModDiscoveryFinished() : EventWrapper<ModDiscoveryFinished.Priority>(nameof(ModDiscoveryFinished))
{
public enum Priority
{
@ -23,11 +22,4 @@ public sealed class ModDiscoveryFinished : EventWrapper<Action, ModDiscoveryFini
/// <seealso cref="Mods.Manager.ModFileSystem.Reload"/>
ModFileSystem = 0,
}
public ModDiscoveryFinished()
: base(nameof(ModDiscoveryFinished))
{ }
public void Invoke()
=> Invoke(this);
}

View file

@ -3,7 +3,7 @@ using OtterGui.Classes;
namespace Penumbra.Communication;
/// <summary> Triggered whenever mods are prepared to be rediscovered. </summary>
public sealed class ModDiscoveryStarted : EventWrapper<Action, ModDiscoveryStarted.Priority>
public sealed class ModDiscoveryStarted() : EventWrapper<ModDiscoveryStarted.Priority>(nameof(ModDiscoveryStarted))
{
public enum Priority
{
@ -16,11 +16,4 @@ public sealed class ModDiscoveryStarted : EventWrapper<Action, ModDiscoveryStart
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.StoreCurrentSelection"/>
ModFileSystemSelector = 200,
}
public ModDiscoveryStarted()
: base(nameof(ModDiscoveryStarted))
{ }
public void Invoke()
=> Invoke(this);
}

View file

@ -13,7 +13,8 @@ namespace Penumbra.Communication;
/// <item>Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. </item>
/// <item>Parameter is the index of the group an option was moved to. </item>
/// </list> </summary>
public sealed class ModOptionChanged : EventWrapper<Action<ModOptionChangeType, Mod, int, int, int>, ModOptionChanged.Priority>
public sealed class ModOptionChanged()
: EventWrapper<ModOptionChangeType, Mod, int, int, int, ModOptionChanged.Priority>(nameof(ModOptionChanged))
{
public enum Priority
{
@ -29,11 +30,4 @@ public sealed class ModOptionChanged : EventWrapper<Action<ModOptionChangeType,
/// <seealso cref="Collections.Manager.CollectionStorage.OnModOptionChange"/>
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);
}

View file

@ -14,7 +14,8 @@ namespace Penumbra.Communication;
/// <item>Parameter is the new directory on addition, move or reload and null on deletion. </item>
/// </list>
/// </summary>
public sealed class ModPathChanged : EventWrapper<Action<ModPathChangeType, Mod, DirectoryInfo?, DirectoryInfo?>, ModPathChanged.Priority>
public sealed class ModPathChanged()
: EventWrapper<ModPathChangeType, Mod, DirectoryInfo?, DirectoryInfo?, ModPathChanged.Priority>(nameof(ModPathChanged))
{
public enum Priority
{
@ -48,11 +49,4 @@ public sealed class ModPathChanged : EventWrapper<Action<ModPathChangeType, Mod,
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnModChangeRemoval"/>
CollectionCacheManagerRemoval = 100,
}
public ModPathChanged()
: base(nameof(ModPathChanged))
{ }
public void Invoke(ModPathChangeType changeType, Mod mod, DirectoryInfo? oldModDirectory, DirectoryInfo? newModDirectory)
=> Invoke(this, changeType, mod, oldModDirectory, newModDirectory);
}

View file

@ -17,7 +17,8 @@ namespace Penumbra.Communication;
/// <item>Parameter is whether the change was inherited from another collection. </item>
/// </list>
/// </summary>
public sealed class ModSettingChanged : EventWrapper<Action<ModCollection, ModSettingChange, Mod?, int, int, bool>, ModSettingChanged.Priority>
public sealed class ModSettingChanged()
: EventWrapper<ModCollection, ModSettingChange, Mod?, int, int, bool, ModSettingChanged.Priority>(nameof(ModSettingChanged))
{
public enum Priority
{
@ -33,11 +34,4 @@ public sealed class ModSettingChanged : EventWrapper<Action<ModCollection, ModSe
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnSettingChange"/>
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);
}

View file

@ -6,18 +6,11 @@ namespace Penumbra.Communication;
/// <item>Parameter is the material resource handle for which the shader package has been loaded. </item>
/// <item>Parameter is the associated game object. </item>
/// </list> </summary>
public sealed class MtrlShpkLoaded : EventWrapper<Action<nint, nint>, MtrlShpkLoaded.Priority>
public sealed class MtrlShpkLoaded() : EventWrapper<nint, nint, MtrlShpkLoaded.Priority>(nameof(MtrlShpkLoaded))
{
public enum Priority
{
/// <seealso cref="Interop.Services.SkinFixer.OnMtrlShpkLoaded"/>
SkinFixer = 0,
}
public MtrlShpkLoaded()
: base(nameof(MtrlShpkLoaded))
{ }
public void Invoke(nint mtrlResourceHandle, nint gameObject)
=> Invoke(this, mtrlResourceHandle, gameObject);
}

View file

@ -8,18 +8,11 @@ namespace Penumbra.Communication;
/// <item>Parameter is the identifier (directory name) of the currently selected mod. </item>
/// </list>
/// </summary>
public sealed class PostSettingsPanelDraw : EventWrapper<Action<string>, PostSettingsPanelDraw.Priority>
public sealed class PostSettingsPanelDraw() : EventWrapper<string, PostSettingsPanelDraw.Priority>(nameof(PostSettingsPanelDraw))
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.PostSettingsPanelDraw"/>
Default = 0,
}
public PostSettingsPanelDraw()
: base(nameof(PostSettingsPanelDraw))
{ }
public void Invoke(string modDirectory)
=> Invoke(this, modDirectory);
}

View file

@ -8,18 +8,11 @@ namespace Penumbra.Communication;
/// <item>Parameter is the identifier (directory name) of the currently selected mod. </item>
/// </list>
/// </summary>
public sealed class PreSettingsPanelDraw : EventWrapper<Action<string>, PreSettingsPanelDraw.Priority>
public sealed class PreSettingsPanelDraw() : EventWrapper<string, PreSettingsPanelDraw.Priority>(nameof(PreSettingsPanelDraw))
{
public enum Priority
{
/// <seealso cref="Api.PenumbraApi.PreSettingsPanelDraw"/>
Default = 0,
}
public PreSettingsPanelDraw()
: base(nameof(PreSettingsPanelDraw))
{ }
public void Invoke(string modDirectory)
=> Invoke(this, modDirectory);
}

View file

@ -15,8 +15,9 @@ namespace Penumbra.Communication;
/// <item>Parameter is the old redirection path for Replaced, or empty. </item>
/// <item>Parameter is the mod responsible for the new redirection if any. </item>
/// </list> </summary>
public sealed class ResolvedFileChanged : EventWrapper<Action<ModCollection, ResolvedFileChanged.Type, Utf8GamePath, FullPath, FullPath, IMod?>,
ResolvedFileChanged.Priority>
public sealed class ResolvedFileChanged()
: EventWrapper<ModCollection, ResolvedFileChanged.Type, Utf8GamePath, FullPath, FullPath, IMod?, ResolvedFileChanged.Priority>(
nameof(ResolvedFileChanged))
{
public enum Type
{
@ -29,14 +30,7 @@ public sealed class ResolvedFileChanged : EventWrapper<Action<ModCollection, Res
public enum Priority
{
/// <seealso cref="Api.DalamudSubstitutionProvider.OnResolvedFileChanged"/>
/// <seealso cref="Api.DalamudSubstitutionProvider.OnResolvedFileChange"/>
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);
}

View file

@ -11,18 +11,11 @@ namespace Penumbra.Communication;
/// <item>Parameter is the selected mod, if any. </item>
/// </list>
/// </summary>
public sealed class SelectTab : EventWrapper<Action<TabType, Mod?>, SelectTab.Priority>
public sealed class SelectTab() : EventWrapper<TabType, Mod?, SelectTab.Priority>(nameof(SelectTab))
{
public enum Priority
{
/// <seealso cref="UI.Tabs.ConfigTabBar.OnSelectTab"/>
ConfigTabBar = 0,
}
public SelectTab()
: base(nameof(SelectTab))
{ }
public void Invoke(TabType tab = TabType.None, Mod? mod = null)
=> Invoke(this, tab, mod);
}

View file

@ -10,7 +10,8 @@ namespace Penumbra.Communication;
/// <item>Parameter is whether the mod was newly created.</item>
/// <item>Parameter is whether the mod was deleted.</item>
/// </list> </summary>
public sealed class TemporaryGlobalModChange : EventWrapper<Action<TemporaryMod, bool, bool>, TemporaryGlobalModChange.Priority>
public sealed class TemporaryGlobalModChange()
: EventWrapper<TemporaryMod, bool, bool, TemporaryGlobalModChange.Priority>(nameof(TemporaryGlobalModChange))
{
public enum Priority
{
@ -20,11 +21,4 @@ public sealed class TemporaryGlobalModChange : EventWrapper<Action<TemporaryMod,
/// <seealso cref="Collections.Manager.TempCollectionManager.OnGlobalModChange"/>
TempCollectionManager = 0,
}
public TemporaryGlobalModChange()
: base(nameof(TemporaryGlobalModChange))
{ }
public void Invoke(TemporaryMod temporaryMod, bool newlyCreated, bool deleted)
=> Invoke(this, temporaryMod, newlyCreated, deleted);
}

View file

@ -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<Queue<nint>> _lastGameObject = new(() => new Queue<nint>());
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<ResolveData> _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<ResolveData> _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
/// <summary> Return the correct resolve data from the stored data. </summary>
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;
}
}

View file

@ -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;
/// <summary> Called for some sound effects caused by animations or VFX. </summary>
public sealed unsafe class ApricotListenerSoundPlay : FastHook<ApricotListenerSoundPlay.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
public ApricotListenerSoundPlay(HookManager hooks, GameState state, CollectionResolver collectionResolver)
{
_state = state;
_collectionResolver = collectionResolver;
Task = hooks.CreateHook<Delegate>("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<nint, GameObject*>**)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;
}
}

View file

@ -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;
/// <summary>
/// Probably used when the base idle animation gets loaded.
/// Make it aware of the correct collection to load the correct pap files.
/// </summary>
public sealed unsafe class CharacterBaseLoadAnimation : FastHook<CharacterBaseLoadAnimation.Delegate>
{
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<Delegate>("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);
}
}

View file

@ -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;
/// <summary> Called for some animations when dismounting. </summary>
public sealed unsafe class Dismount : FastHook<Dismount.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
public Dismount(HookManager hooks, GameState state, CollectionResolver collectionResolver)
{
_state = state;
_collectionResolver = collectionResolver;
Task = hooks.CreateHook<Delegate>("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);
}
}

View file

@ -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;
/// <summary> Load a ground-based area VFX. </summary>
public sealed unsafe class LoadAreaVfx : FastHook<LoadAreaVfx.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
public LoadAreaVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver)
{
_state = state;
_collectionResolver = collectionResolver;
Task = hooks.CreateHook<Delegate>("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;
}
}

View file

@ -0,0 +1,34 @@
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.Interop.PathResolving;
namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Characters load some of their voice lines or whatever with this function. </summary>
public sealed unsafe class LoadCharacterSound : FastHook<LoadCharacterSound.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
public LoadCharacterSound(HookManager hooks, GameState state, CollectionResolver collectionResolver)
{
_state = state;
_collectionResolver = collectionResolver;
Task = hooks.CreateHook<Delegate>("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;
}
}

View file

@ -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;
/// <summary> Load a VFX specifically for a character. </summary>
public sealed unsafe class LoadCharacterVfx : FastHook<LoadCharacterVfx.Delegate>
{
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<Delegate>("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;
}
/// <summary> Search an object by its id, then get its minion/mount/ornament. </summary>
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];
}
}

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
public sealed unsafe class LoadTimelineResources : FastHook<LoadTimelineResources.Delegate>
{
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<Delegate>("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;
}
/// <summary> Use timelines vfuncs to obtain the associated game object. </summary>
public static ResolveData GetDataFromTimeline(IObjectTable objects, CollectionResolver resolver, nint timeline)
{
try
{
if (timeline != nint.Zero)
{
var getGameObjectIdx = ((delegate* unmanaged<nint, int>**)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;
}
}

View file

@ -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<PlayFootstep.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
public PlayFootstep(HookManager hooks, GameState state, CollectionResolver collectionResolver)
{
_state = state;
_collectionResolver = collectionResolver;
Task = hooks.CreateHook<Delegate>("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);
}
}

View file

@ -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;
/// <summary> Called when some action timelines update. </summary>
public sealed unsafe class ScheduleClipUpdate : FastHook<ScheduleClipUpdate.Delegate>
{
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<Delegate>("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);
}
}

View file

@ -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;
/// <summary> Seems to load character actions when zoning or changing class, maybe. </summary>
public sealed unsafe class SomeActionLoad : FastHook<SomeActionLoad.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
public SomeActionLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver)
{
_state = state;
_collectionResolver = collectionResolver;
Task = hooks.CreateHook<Delegate>("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);
}
}

View file

@ -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;
/// <summary> Called for some animations when mounted or mounting. </summary>
public sealed unsafe class SomeMountAnimation : FastHook<SomeMountAnimation.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
public SomeMountAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver)
{
_state = state;
_collectionResolver = collectionResolver;
Task = hooks.CreateHook<Delegate>("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);
}
}

View file

@ -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;
/// <summary> Unknown what exactly this is, but it seems to load a bunch of paps. </summary>
public sealed unsafe class SomePapLoad : FastHook<SomePapLoad.Delegate>
{
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<Delegate>("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);
}
}

View file

@ -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;
/// <summary> Called for some animations when using a Parasol. </summary>
public sealed unsafe class SomeParasolAnimation : FastHook<SomeParasolAnimation.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
public SomeParasolAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver)
{
_state = state;
_collectionResolver = collectionResolver;
Task = hooks.CreateHook<Delegate>("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);
}
}

View file

@ -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<CharacterBase, CharacterBaseDestructor.Priority>, IHookService
{
public enum Priority
{
/// <seealso cref="PathResolving.DrawObjectState"/>
DrawObjectState = 0,
/// <seealso cref="ModEditWindow.MtrlTab"/>
MtrlTab = -1000,
}
public CharacterBaseDestructor(HookManager hooks)
: base("Destroy CharacterBase")
=> _task = hooks.CreateHook<Delegate>(Name, Address, Detour, true);
private readonly Task<Hook<Delegate>> _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);
}
}

View file

@ -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<Character, CharacterDestructor.Priority>, IHookService
{
public enum Priority
{
/// <seealso cref="PathResolving.CutsceneService"/>
CutsceneService = 0,
/// <seealso cref="PathResolving.IdentifiedCollectionCache"/>
IdentifiedCollectionCache = 0,
}
public CharacterDestructor(HookManager hooks)
: base("Character Destructor")
=> _task = hooks.CreateHook<Delegate>(Name, Sigs.CharacterDestructor, Detour, true);
private readonly Task<Hook<Delegate>> _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);
}
}

View file

@ -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<Character, Character, CopyCharacter.Priority>, IHookService
{
public enum Priority
{
/// <seealso cref="PathResolving.CutsceneService"/>
CutsceneService = 0,
}
public CopyCharacter(HookManager hooks)
: base("Copy Character")
=> _task = hooks.CreateHook<Delegate>(Name, Address, Detour, true);
private readonly Task<Hook<Delegate>> _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);
}
}

View file

@ -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<ModelCharaId, CustomizeArray, CharacterArmor, CreateCharacterBase.Priority>, IHookService
{
public enum Priority
{
/// <seealso cref="PathResolving.MetaState"/>
MetaState = 0,
}
public CreateCharacterBase(HookManager hooks)
: base("Create CharacterBase")
=> _task = hooks.CreateHook<Delegate>(Name, Address, Detour, true);
private readonly Task<Hook<Delegate>> _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<ModelCharaId, CustomizeArray, CharacterArmor, CharacterBase> subscriber, PostEvent.Priority priority)
=> _postEvent.Subscribe(subscriber, priority);
public void Unsubscribe(ActionPtr234<ModelCharaId, CustomizeArray, CharacterArmor, CharacterBase> 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<ModelCharaId, CustomizeArray, CharacterArmor, CharacterBase, PostEvent.Priority>(name)
{
public enum Priority
{
/// <seealso cref="PathResolving.DrawObjectState"/>
DrawObjectState = 0,
/// <seealso cref="PathResolving.MetaState"/>
MetaState = 0,
}
}
}

View file

@ -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<Delegate>("Debug Hook", Signature, Detour, true);
}
private readonly Task<Hook<Delegate>>? _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

View file

@ -0,0 +1,48 @@
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.GameData;
namespace Penumbra.Interop.Hooks;
/// <summary>
/// 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.
/// </summary>
public sealed unsafe class EnableDraw : IHookService
{
private readonly Task<Hook<Delegate>> _task;
private readonly GameState _state;
public EnableDraw(HookManager hooks, GameState state)
{
_state = state;
_task = hooks.CreateHook<Delegate>("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();
}

View file

@ -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<ResourceHandle, ResourceHandleDestructor.Priority>, IHookService
{
public enum Priority
{
/// <seealso cref="PathResolving.SubfileHelper"/>
SubfileHelper,
/// <seealso cref="SkinFixer"/>
SkinFixer,
}
public ResourceHandleDestructor(HookManager hooks)
: base("Destroy ResourceHandle")
=> _task = hooks.CreateHook<Delegate>(Name, Sigs.ResourceHandleDestructor, Detour, true);
private readonly Task<Hook<Delegate>> _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);
}
}

View file

@ -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<DrawDataContainer, Character, CharacterWeapon, WeaponReload.Priority>, IHookService
{
public enum Priority
{
/// <seealso cref="PathResolving.DrawObjectState"/>
DrawObjectState = 0,
}
public WeaponReload(HookManager hooks)
: base("Reload Weapon")
=> _task = hooks.CreateHook<Delegate>(Name, Address, Detour, true);
private readonly Task<Hook<Delegate>> _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<DrawDataContainer, Character> subscriber, PostEvent.Priority priority)
=> _postEvent.Subscribe(subscriber, priority);
public void Unsubscribe(ActionPtr<DrawDataContainer, Character> 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<DrawDataContainer, Character, PostEvent.Priority>(name)
{
public enum Priority
{
/// <seealso cref="PathResolving.DrawObjectState"/>
DrawObjectState = 0,
}
}
}

View file

@ -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<ResolveData> _animationLoadData = new(() => ResolveData.Invalid, true);
private readonly ThreadLocal<ResolveData> _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<LoadCharacterSound>(
(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();
}
/// <summary> Characters load some of their voice lines or whatever with this function. </summary>
private delegate nint LoadCharacterSound(nint character, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7);
private readonly Hook<LoadCharacterSound> _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;
}
/// <summary>
/// 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.
/// </summary>
private delegate ulong LoadTimelineResourcesDelegate(nint timeline);
[Signature(Sigs.LoadTimelineResources, DetourName = nameof(LoadTimelineResourcesDetour))]
private readonly Hook<LoadTimelineResourcesDelegate> _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;
}
/// <summary>
/// Probably used when the base idle animation gets loaded.
/// Make it aware of the correct collection to load the correct pap files.
/// </summary>
private delegate void CharacterBaseNoArgumentDelegate(nint drawBase);
[Signature(Sigs.CharacterBaseLoadAnimation, DetourName = nameof(CharacterBaseLoadAnimationDetour))]
private readonly Hook<CharacterBaseNoArgumentDelegate> _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;
}
/// <summary> Unknown what exactly this is but it seems to load a bunch of paps. </summary>
private delegate void LoadSomePap(nint a1, int a2, nint a3, int a4);
[Signature(Sigs.LoadSomePap, DetourName = nameof(LoadSomePapDetour))]
private readonly Hook<LoadSomePap> _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);
/// <summary> Seems to load character actions when zoning or changing class, maybe. </summary>
[Signature(Sigs.LoadSomeAction, DetourName = nameof(SomeActionLoadDetour))]
private readonly Hook<SomeActionLoadDelegate> _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;
}
/// <summary> Load a VFX specifically for a character. </summary>
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<LoadCharacterVfxDelegate> _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;
}
/// <summary> Load a ground-based area VFX. </summary>
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<LoadAreaVfxDelegate> _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;
}
/// <summary> Called when some action timelines update. </summary>
private delegate void ScheduleClipUpdate(ClipScheduler* x);
[Signature(Sigs.ScheduleClipUpdate, DetourName = nameof(ScheduleClipUpdateDetour))]
private readonly Hook<ScheduleClipUpdate> _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;
}
/// <summary> Search an object by its id, then get its minion/mount/ornament. </summary>
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];
}
/// <summary> Use timelines vfuncs to obtain the associated game object. </summary>
private ResolveData GetDataFromTimeline(nint timeline)
{
try
{
if (timeline != nint.Zero)
{
var getGameObjectIdx = ((delegate* unmanaged<nint, int>**)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<UnkMountAnimationDelegate> _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<UnkParasolAnimationDelegate> _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<DismountDelegate> _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<ApricotListenerSoundPlayDelegate> _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<nint, GameObject*>**)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<FootStepDelegate> _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;
}
}

View file

@ -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;
}
/// <summary>
/// Get the collection applying to the current player character
/// or the Yourself or Default collection if no player exists.
/// </summary>
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;
}
/// <summary> Identify the correct collection for a game object. </summary>
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);
}
}
/// <summary> Identify the correct collection for the last created game object. </summary>
public ResolveData IdentifyLastGameObjectCollection(bool useCache)
=> IdentifyCollection((GameObject*)_drawObjectState.LastGameObject, useCache);
=> IdentifyCollection((GameObject*)drawObjectState.LastGameObject, useCache);
/// <summary> Identify the correct collection for a draw object. </summary>
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);
}
/// <summary> Return whether the given ModelChara id refers to a human-type model. </summary>
public bool IsModelHuman(uint modelCharaId)
=> _humanModels.IsHuman(modelCharaId);
=> humanModels.IsHuman(modelCharaId);
/// <summary> Return whether the given character has a human model. </summary>
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;
}
/// <summary> Used if at the aesthetician. The relevant actor is yourself, so use player collection when possible. </summary>
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
/// </summary>
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);
}
/// <summary> Check both temporary and permanent character collections. Temporary first. </summary>
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;
}
/// <summary> Get the collection applying to the owner if it is available. </summary>
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)

View file

@ -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<KeyValuePair<int, Dalamud.Game.ClientState.Objects.Types.GameObject>> 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);
}
/// <summary>
@ -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)

View file

@ -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<nint, (nint, bool)>
public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (nint, bool)>, 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<nint, (nint GameObject, bool IsChild)> _drawObjectToGameObject = new();
private readonly ThreadLocal<Queue<nint>> _lastGameObject = new(() => new Queue<nint>());
private readonly Dictionary<nint, (nint GameObject, bool IsChild)> _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<nint, (nint, boo
public IEnumerable<(nint, bool)> 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);
}
/// <summary>
@ -123,20 +127,4 @@ public class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (nint, boo
prevSibling = prevSibling->PreviousSiblingObject;
}
}
/// <summary>
/// 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.
/// </summary>
private delegate void EnableDrawDelegate(nint gameObject);
[Signature(Sigs.EnableDraw, DetourName = nameof(EnableDrawDetour))]
private readonly Hook<EnableDrawDelegate> _enableDrawHook = null!;
private void EnableDrawDetour(nint gameObject)
{
_lastGameObject.Value!.Enqueue(gameObject);
_enableDrawHook.Original.Invoke(gameObject);
_lastGameObject.Value!.TryDequeue(out _);
}
}

View file

@ -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<nint, (ActorIdentifier, ModCollection)> _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);
}

View file

@ -9,6 +9,8 @@ using Penumbra.Collections;
using Penumbra.Api.Enums;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Hooks;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.Services;
using Penumbra.Services;
@ -52,24 +54,24 @@ public unsafe class MetaState : IDisposable
private readonly PerformanceTracker _performance;
private readonly CollectionResolver _collectionResolver;
private readonly ResourceLoader _resources;
private readonly GameEventManager _gameEventManager;
private readonly CharacterUtility _characterUtility;
private readonly CreateCharacterBase _createCharacterBase;
private ResolveData _lastCreatedCollection = ResolveData.Invalid;
private ResolveData _customizeChangeCollection = ResolveData.Invalid;
private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty;
public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver,
ResourceLoader resources, GameEventManager gameEventManager, CharacterUtility characterUtility, Configuration config,
ResourceLoader resources, CreateCharacterBase createCharacterBase, CharacterUtility characterUtility, Configuration config,
IGameInteropProvider interop)
{
_performance = performance;
_communicator = communicator;
_collectionResolver = collectionResolver;
_resources = resources;
_gameEventManager = gameEventManager;
_characterUtility = characterUtility;
_config = config;
_performance = performance;
_communicator = communicator;
_collectionResolver = collectionResolver;
_resources = resources;
_createCharacterBase = createCharacterBase;
_characterUtility = characterUtility;
_config = config;
interop.InitializeFromAttributes(this);
_calculateHeightHook =
interop.HookFromAddress<CalculateHeightDelegate>((nint)Character.MemberFunctionPointers.CalculateHeight, CalculateHeightDetour);
@ -81,8 +83,8 @@ public unsafe class MetaState : IDisposable
_rspSetupCharacterHook.Enable();
_changeCustomize.Enable();
_calculateHeightHook.Enable();
_gameEventManager.CreatingCharacterBase += OnCreatingCharacterBase;
_gameEventManager.CharacterBaseCreated += OnCharacterBaseCreated;
_createCharacterBase.Subscribe(OnCreatingCharacterBase, CreateCharacterBase.Priority.MetaState);
_createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.MetaState);
}
public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData)
@ -124,31 +126,31 @@ public unsafe class MetaState : IDisposable
_rspSetupCharacterHook.Dispose();
_changeCustomize.Dispose();
_calculateHeightHook.Dispose();
_gameEventManager.CreatingCharacterBase -= OnCreatingCharacterBase;
_gameEventManager.CharacterBaseCreated -= OnCharacterBaseCreated;
_createCharacterBase.Unsubscribe(OnCreatingCharacterBase);
_createCharacterBase.Unsubscribe(OnCharacterBaseCreated);
}
private void OnCreatingCharacterBase(nint modelCharaId, nint customize, nint equipData)
private void OnCreatingCharacterBase(ModelCharaId* modelCharaId, CustomizeArray* customize, CharacterArmor* equipData)
{
_lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true);
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero)
_communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject,
_lastCreatedCollection.ModCollection.Name, modelCharaId, customize, equipData);
_lastCreatedCollection.ModCollection.Name, (nint) modelCharaId, (nint) customize, (nint) equipData);
var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection,
UsesDecal(*(uint*)modelCharaId, customize));
UsesDecal(*(uint*)modelCharaId, (nint) customize));
var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility);
_characterBaseCreateMetaChanges.Dispose(); // Should always be empty.
_characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp);
}
private void OnCharacterBaseCreated(uint _1, nint _2, nint _3, nint drawObject)
private void OnCharacterBaseCreated(ModelCharaId _1, CustomizeArray* _2, CharacterArmor* _3, CharacterBase* drawObject)
{
_characterBaseCreateMetaChanges.Dispose();
_characterBaseCreateMetaChanges = DisposableContainer.Empty;
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != nint.Zero)
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != null)
_communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject,
_lastCreatedCollection.ModCollection, drawObject);
_lastCreatedCollection.ModCollection, (nint)drawObject);
_lastCreatedCollection = ResolveData.Invalid;
}

View file

@ -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;
}
/// <summary> Obtain a temporary or permanent collection by name. </summary>
@ -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();

View file

@ -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;
/// </summary>
public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePair<nint, ResolveData>>
{
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<ResolveData> _mtrlData = new(() => ResolveData.Invalid);
private readonly ThreadLocal<ResolveData> _avfxData = new(() => ResolveData.Invalid);
private readonly ConcurrentDictionary<nint, ResolveData> _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<KeyValuePai
public void Dispose()
{
_loader.ResourceLoaded -= SubfileContainerRequested;
_events.ResourceHandleDestructor -= ResourceDestroyed;
_resourceHandleDestructor.Unsubscribe(ResourceDestroyed);
_loadMtrlShpkHook.Dispose();
_loadMtrlTexHook.Dispose();
_apricotResourceLoadHook.Dispose();

View file

@ -1,299 +0,0 @@
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using Penumbra.GameData;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.Interop.Structs;
namespace Penumbra.Interop.Services;
public unsafe class GameEventManager : IDisposable
{
private const string Prefix = $"[{nameof(GameEventManager)}]";
public event CharacterDestructorEvent? CharacterDestructor;
public event CopyCharacterEvent? CopyCharacter;
public event ResourceHandleDestructorEvent? ResourceHandleDestructor;
public event CreatingCharacterBaseEvent? CreatingCharacterBase;
public event CharacterBaseCreatedEvent? CharacterBaseCreated;
public event CharacterBaseDestructorEvent? CharacterBaseDestructor;
public event WeaponReloadingEvent? WeaponReloading;
public event WeaponReloadedEvent? WeaponReloaded;
public GameEventManager(IGameInteropProvider interop)
{
interop.InitializeFromAttributes(this);
_copyCharacterHook =
interop.HookFromAddress<CopyCharacterDelegate>((nint)CharacterSetup.MemberFunctionPointers.CopyFromCharacter, CopyCharacterDetour);
_characterBaseCreateHook =
interop.HookFromAddress<CharacterBaseCreateDelegate>((nint)CharacterBase.MemberFunctionPointers.Create, CharacterBaseCreateDetour);
_characterBaseDestructorHook =
interop.HookFromAddress<CharacterBaseDestructorEvent>((nint)CharacterBase.MemberFunctionPointers.Destroy,
CharacterBaseDestructorDetour);
_weaponReloadHook =
interop.HookFromAddress<WeaponReloadFunc>((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<CharacterDestructorDelegate> _characterDtorHook = null!;
private void CharacterDestructorDetour(Character* character)
{
if (CharacterDestructor != null)
foreach (var subscriber in CharacterDestructor.GetInvocationList())
{
try
{
((CharacterDestructorEvent)subscriber).Invoke(character);
}
catch (Exception ex)
{
Penumbra.Log.Error($"{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<CopyCharacterDelegate> _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<ResourceHandleDestructorDelegate> _resourceHandleDestructorHook = null!;
private IntPtr ResourceHandleDestructorDetour(ResourceHandle* handle)
{
if (ResourceHandleDestructor != null)
foreach (var subscriber in ResourceHandleDestructor.GetInvocationList())
{
try
{
((ResourceHandleDestructorEvent)subscriber).Invoke(handle);
}
catch (Exception ex)
{
Penumbra.Log.Error(
$"{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<CharacterBaseCreateDelegate> _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<CharacterBaseDestructorEvent> _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<WeaponReloadFunc> _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<TestDelegate>? _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
}

View file

@ -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<OnRenderMaterialDelegate> _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<nint> _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<OnRenderMaterialDelegate>(_humanVTable[62], OnRenderHumanMaterial);
_resourceHandleDestructor = resourceHandleDestructor;
_utility = utility;
_communicator = communicator;
_onRenderMaterialHook = interop.HookFromAddress<OnRenderMaterialDelegate>(_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;
}

View file

@ -2,7 +2,6 @@ using Dalamud.Utility;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Mods.Manager;

View file

@ -80,6 +80,10 @@ public class Penumbra : IDalamudPlugin
_services.GetService<SkinFixer>();
_services.GetService<DalamudSubstitutionProvider>(); // Initialize before Interface.
foreach (var service in _services.GetServicesImplementing<IAwaitedService>())
service.Awaiter.Wait();
SetupInterface();
SetupApi();

View file

@ -69,6 +69,7 @@
<ItemGroup>
<PackageReference Include="EmbedIO" Version="3.4.3" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.2" />
<PackageReference Include="SharpCompress" Version="0.33.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
@ -92,8 +93,8 @@
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="GitCommitHashSuccess"/>
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" Condition="$(GitCommitHashSuccess) == 0"/>
<Output TaskParameter="ExitCode" PropertyName="GitCommitHashSuccess" />
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" Condition="$(GitCommitHashSuccess) == 0" />
</Exec>
<PropertyGroup>

View file

@ -9,7 +9,7 @@ public class CommunicatorService : IDisposable, IService
{
public CommunicatorService(Logger logger)
{
EventWrapper.ChangeLogger(logger);
EventWrapperBase.ChangeLogger(logger);
}
/// <inheritdoc cref="Communication.CollectionChange"/>

View file

@ -82,8 +82,7 @@ public static class ServiceManagerA
.AddDalamudService<IPluginLog>(pi);
private static ServiceManager AddInterop(this ServiceManager services)
=> services.AddSingleton<GameEventManager>()
.AddSingleton<FrameworkManager>()
=> services.AddSingleton<FrameworkManager>()
.AddSingleton<CutsceneService>()
.AddSingleton(p =>
{
@ -135,8 +134,7 @@ public static class ServiceManagerA
.AddSingleton<SkinFixer>();
private static ServiceManager AddResolvers(this ServiceManager services)
=> services.AddSingleton<AnimationHookService>()
.AddSingleton<CollectionResolver>()
=> services.AddSingleton<CollectionResolver>()
.AddSingleton<CutsceneService>()
.AddSingleton<DrawObjectState>()
.AddSingleton<MetaState>()

View file

@ -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

View file

@ -13,8 +13,8 @@ using Penumbra.Communication;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.Import.Textures;
using Penumbra.Interop.Hooks;
using Penumbra.Interop.ResourceTree;
using Penumbra.Interop.Services;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
@ -32,20 +32,20 @@ public partial class ModEditWindow : Window, IDisposable
{
private const string WindowBaseLabel = "###SubModEdit";
private readonly PerformanceTracker _performance;
private readonly ModEditor _editor;
private readonly Configuration _config;
private readonly ItemSwapTab _itemSwapTab;
private readonly MetaFileManager _metaFileManager;
private readonly ActiveCollections _activeCollections;
private readonly StainService _stainService;
private readonly ModMergeTab _modMergeTab;
private readonly CommunicatorService _communicator;
private readonly IDragDropManager _dragDropManager;
private readonly GameEventManager _gameEvents;
private readonly IDataManager _gameData;
private readonly IFramework _framework;
private readonly IObjectTable _objects;
private readonly PerformanceTracker _performance;
private readonly ModEditor _editor;
private readonly Configuration _config;
private readonly ItemSwapTab _itemSwapTab;
private readonly MetaFileManager _metaFileManager;
private readonly ActiveCollections _activeCollections;
private readonly StainService _stainService;
private readonly ModMergeTab _modMergeTab;
private readonly CommunicatorService _communicator;
private readonly IDragDropManager _dragDropManager;
private readonly IDataManager _gameData;
private readonly IFramework _framework;
private readonly IObjectTable _objects;
private readonly CharacterBaseDestructor _characterBaseDestructor;
private Mod? _mod;
private Vector2 _iconSize = Vector2.Zero;
@ -565,26 +565,26 @@ public partial class ModEditWindow : Window, IDisposable
public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData,
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager,
StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab,
CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents,
ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework)
CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager,
ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework, CharacterBaseDestructor characterBaseDestructor)
: base(WindowBaseLabel)
{
_performance = performance;
_itemSwapTab = itemSwapTab;
_gameData = gameData;
_config = config;
_editor = editor;
_metaFileManager = metaFileManager;
_stainService = stainService;
_activeCollections = activeCollections;
_modMergeTab = modMergeTab;
_communicator = communicator;
_dragDropManager = dragDropManager;
_textures = textures;
_fileDialog = fileDialog;
_gameEvents = gameEvents;
_objects = objects;
_framework = framework;
_performance = performance;
_itemSwapTab = itemSwapTab;
_gameData = gameData;
_config = config;
_editor = editor;
_metaFileManager = metaFileManager;
_stainService = stainService;
_activeCollections = activeCollections;
_modMergeTab = modMergeTab;
_communicator = communicator;
_dragDropManager = dragDropManager;
_textures = textures;
_fileDialog = fileDialog;
_objects = objects;
_framework = framework;
_characterBaseDestructor = characterBaseDestructor;
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
(bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable));
@ -598,12 +598,12 @@ public partial class ModEditWindow : Window, IDisposable
_resourceTreeFactory = resourceTreeFactory;
_quickImportViewer =
new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions);
_communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow);
}
public void Dispose()
{
_communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_editor?.Dispose();
_materialTab.Dispose();
_modelTab.Dispose();
@ -613,7 +613,7 @@ public partial class ModEditWindow : Window, IDisposable
_center.Dispose();
}
private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2)
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2)
{
if (type is ModPathChangeType.Reloaded or ModPathChangeType.Moved)
ChangeMod(mod);

View file

@ -11,6 +11,18 @@
"Unosquare.Swan.Lite": "3.0.0"
}
},
"Microsoft.CodeAnalysis.Common": {
"type": "Direct",
"requested": "[4.8.0, )",
"resolved": "4.8.0",
"contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==",
"dependencies": {
"Microsoft.CodeAnalysis.Analyzers": "3.3.4",
"System.Collections.Immutable": "7.0.0",
"System.Reflection.Metadata": "7.0.0",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Direct",
"requested": "[7.0.0, )",
@ -36,6 +48,11 @@
"System.Text.Encoding.CodePages": "5.0.0"
}
},
"Microsoft.CodeAnalysis.Analyzers": {
"type": "Transitive",
"resolved": "3.3.4",
"contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "7.0.0",
@ -46,10 +63,23 @@
"resolved": "5.0.0",
"contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ=="
},
"System.Collections.Immutable": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ=="
},
"System.Reflection.Metadata": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==",
"dependencies": {
"System.Collections.Immutable": "7.0.0"
}
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA=="
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"System.Text.Encoding.CodePages": {
"type": "Transitive",