mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Add new cutscene ENPC tracking hooks.
This commit is contained in:
parent
7b517390b6
commit
8779f4b689
7 changed files with 152 additions and 11 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit c525072299d5febd2bb638ab229060b0073ba6a6
|
Subproject commit 5bac66e5ad73e57919aff7f8b046606b75e191a2
|
||||||
|
|
@ -11,7 +11,8 @@ public class GameState : IService
|
||||||
{
|
{
|
||||||
#region Last Game Object
|
#region Last Game Object
|
||||||
|
|
||||||
private readonly ThreadLocal<Queue<nint>> _lastGameObject = new(() => new Queue<nint>());
|
private readonly ThreadLocal<Queue<nint>> _lastGameObject = new(() => new Queue<nint>());
|
||||||
|
public readonly ThreadLocal<bool> CharacterAssociated = new(() => false);
|
||||||
|
|
||||||
public nint LastGameObject
|
public nint LastGameObject
|
||||||
=> _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero;
|
=> _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero;
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,8 @@ public class HookOverrides
|
||||||
public bool CreateCharacterBase;
|
public bool CreateCharacterBase;
|
||||||
public bool EnableDraw;
|
public bool EnableDraw;
|
||||||
public bool WeaponReload;
|
public bool WeaponReload;
|
||||||
|
public bool SetupPlayerNpc;
|
||||||
|
public bool ConstructCutsceneCharacter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct PostProcessingHooks
|
public struct PostProcessingHooks
|
||||||
|
|
|
||||||
70
Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs
Normal file
70
Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Interop;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Objects;
|
||||||
|
|
||||||
|
public sealed unsafe class ConstructCutsceneCharacter : EventWrapperPtr<Character, ConstructCutsceneCharacter.Priority>, IHookService
|
||||||
|
{
|
||||||
|
private readonly GameState _gameState;
|
||||||
|
private readonly ObjectManager _objects;
|
||||||
|
|
||||||
|
public enum Priority
|
||||||
|
{
|
||||||
|
/// <seealso cref="PathResolving.CutsceneService.OnSetupPlayerNpc"/>
|
||||||
|
CutsceneService = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConstructCutsceneCharacter(GameState gameState, HookManager hooks, ObjectManager objects)
|
||||||
|
: base("ConstructCutsceneCharacter")
|
||||||
|
{
|
||||||
|
_gameState = gameState;
|
||||||
|
_objects = objects;
|
||||||
|
_task = hooks.CreateHook<Delegate>(Name, Sigs.ConstructCutsceneCharacter, Detour, !HookOverrides.Instance.Objects.ConstructCutsceneCharacter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Task<Hook<Delegate>> _task;
|
||||||
|
|
||||||
|
public delegate int Delegate(SetupPlayerNpc.SchedulerStruct* scheduler);
|
||||||
|
|
||||||
|
public int Detour(SetupPlayerNpc.SchedulerStruct* scheduler)
|
||||||
|
{
|
||||||
|
// This is the function that actually creates the new game object
|
||||||
|
// and fills it into the object table at a free index etc.
|
||||||
|
var ret = _task.Result.Original(scheduler);
|
||||||
|
// Check for the copy state from SetupPlayerNpc.
|
||||||
|
if (_gameState.CharacterAssociated.Value)
|
||||||
|
{
|
||||||
|
// If the newly created character exists, invoke the event.
|
||||||
|
var character = _objects[ret + (int)ScreenActor.CutsceneStart].AsCharacter;
|
||||||
|
if (character != null)
|
||||||
|
{
|
||||||
|
Invoke(character);
|
||||||
|
Penumbra.Log.Verbose(
|
||||||
|
$"[{Name}] Created indirect copy of player character at 0x{(nint)character}, index {character->ObjectIndex}.");
|
||||||
|
}
|
||||||
|
_gameState.CharacterAssociated.Value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntPtr 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;
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,7 @@ public sealed unsafe class EnableDraw : IHookService
|
||||||
private void Detour(GameObject* gameObject)
|
private void Detour(GameObject* gameObject)
|
||||||
{
|
{
|
||||||
_state.QueueGameObject(gameObject);
|
_state.QueueGameObject(gameObject);
|
||||||
Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint)gameObject:X}.");
|
Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint)gameObject:X} at {gameObject->ObjectIndex}.");
|
||||||
_task.Result.Original.Invoke(gameObject);
|
_task.Result.Original.Invoke(gameObject);
|
||||||
_state.DequeueGameObject();
|
_state.DequeueGameObject();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs
Normal file
55
Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Objects;
|
||||||
|
|
||||||
|
public sealed unsafe class SetupPlayerNpc : FastHook<SetupPlayerNpc.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _gameState;
|
||||||
|
|
||||||
|
public SetupPlayerNpc(GameState gameState, HookManager hooks)
|
||||||
|
{
|
||||||
|
_gameState = gameState;
|
||||||
|
Task = hooks.CreateHook<Delegate>("SetupPlayerNPC", Sigs.SetupPlayerNpc, Detour,
|
||||||
|
!HookOverrides.Instance.Objects.SetupPlayerNpc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate SchedulerStruct* Delegate(byte* npcType, nint unk, NpcSetupData* setupData);
|
||||||
|
|
||||||
|
public SchedulerStruct* Detour(byte* npcType, nint unk, NpcSetupData* setupData)
|
||||||
|
{
|
||||||
|
// This function actually seems to generate all NPC.
|
||||||
|
|
||||||
|
// If an ENPC is being created, check the creation parameters.
|
||||||
|
// If CopyPlayerCustomize is true, the event NPC gets a timeline that copies its customize and glasses from the local player.
|
||||||
|
// Keep track of this, so we can associate the actor to be created for this with the player character, see ConstructCutsceneCharacter.
|
||||||
|
if (setupData->CopyPlayerCustomize && npcType != null && *npcType is 8)
|
||||||
|
_gameState.CharacterAssociated.Value = true;
|
||||||
|
|
||||||
|
var ret = Task.Result.Original.Invoke(npcType, unk, setupData);
|
||||||
|
Penumbra.Log.Excessive(
|
||||||
|
$"[Setup Player NPC] Invoked for type {*npcType} with 0x{unk:X} and Copy Player Customize: {setupData->CopyPlayerCustomize}.");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
public struct NpcSetupData
|
||||||
|
{
|
||||||
|
[FieldOffset(0x0B)]
|
||||||
|
private byte _copyPlayerCustomize;
|
||||||
|
|
||||||
|
public bool CopyPlayerCustomize
|
||||||
|
{
|
||||||
|
get => _copyPlayerCustomize != 0;
|
||||||
|
set => _copyPlayerCustomize = value ? (byte)1 : (byte)0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
public struct SchedulerStruct
|
||||||
|
{
|
||||||
|
public static Character* GetCharacter(SchedulerStruct* s)
|
||||||
|
=> ((delegate* unmanaged<SchedulerStruct*, Character*>**)s)[0][19](s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,10 +15,11 @@ public sealed class CutsceneService : IRequiredService, IDisposable
|
||||||
public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd;
|
public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd;
|
||||||
public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx;
|
public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx;
|
||||||
|
|
||||||
private readonly ObjectManager _objects;
|
private readonly ObjectManager _objects;
|
||||||
private readonly CopyCharacter _copyCharacter;
|
private readonly CopyCharacter _copyCharacter;
|
||||||
private readonly CharacterDestructor _characterDestructor;
|
private readonly CharacterDestructor _characterDestructor;
|
||||||
private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray();
|
private readonly ConstructCutsceneCharacter _constructCutsceneCharacter;
|
||||||
|
private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray();
|
||||||
|
|
||||||
public IEnumerable<KeyValuePair<int, IGameObject>> Actors
|
public IEnumerable<KeyValuePair<int, IGameObject>> Actors
|
||||||
=> Enumerable.Range(CutsceneStartIdx, CutsceneSlots)
|
=> Enumerable.Range(CutsceneStartIdx, CutsceneSlots)
|
||||||
|
|
@ -26,13 +27,15 @@ public sealed class CutsceneService : IRequiredService, IDisposable
|
||||||
.Select(i => KeyValuePair.Create(i, this[i] ?? _objects.GetDalamudObject(i)!));
|
.Select(i => KeyValuePair.Create(i, this[i] ?? _objects.GetDalamudObject(i)!));
|
||||||
|
|
||||||
public unsafe CutsceneService(ObjectManager objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor,
|
public unsafe CutsceneService(ObjectManager objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor,
|
||||||
IClientState clientState)
|
ConstructCutsceneCharacter constructCutsceneCharacter, IClientState clientState)
|
||||||
{
|
{
|
||||||
_objects = objects;
|
_objects = objects;
|
||||||
_copyCharacter = copyCharacter;
|
_copyCharacter = copyCharacter;
|
||||||
_characterDestructor = characterDestructor;
|
_characterDestructor = characterDestructor;
|
||||||
|
_constructCutsceneCharacter = constructCutsceneCharacter;
|
||||||
_copyCharacter.Subscribe(OnCharacterCopy, CopyCharacter.Priority.CutsceneService);
|
_copyCharacter.Subscribe(OnCharacterCopy, CopyCharacter.Priority.CutsceneService);
|
||||||
_characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.CutsceneService);
|
_characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.CutsceneService);
|
||||||
|
_constructCutsceneCharacter.Subscribe(OnSetupPlayerNpc, ConstructCutsceneCharacter.Priority.CutsceneService);
|
||||||
if (clientState.IsGPosing)
|
if (clientState.IsGPosing)
|
||||||
RecoverGPoseActors();
|
RecoverGPoseActors();
|
||||||
}
|
}
|
||||||
|
|
@ -87,6 +90,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable
|
||||||
{
|
{
|
||||||
_copyCharacter.Unsubscribe(OnCharacterCopy);
|
_copyCharacter.Unsubscribe(OnCharacterCopy);
|
||||||
_characterDestructor.Unsubscribe(OnCharacterDestructor);
|
_characterDestructor.Unsubscribe(OnCharacterDestructor);
|
||||||
|
_constructCutsceneCharacter.Unsubscribe(OnSetupPlayerNpc);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void OnCharacterDestructor(Character* character)
|
private unsafe void OnCharacterDestructor(Character* character)
|
||||||
|
|
@ -124,6 +128,15 @@ public sealed class CutsceneService : IRequiredService, IDisposable
|
||||||
_copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1);
|
_copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unsafe void OnSetupPlayerNpc(Character* npc)
|
||||||
|
{
|
||||||
|
if (npc == null || npc->ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var idx = npc->GameObject.ObjectIndex - CutsceneStartIdx;
|
||||||
|
_copiedCharacters[idx] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary> Try to recover GPose actors on reloads into a running game. </summary>
|
/// <summary> Try to recover GPose actors on reloads into a running game. </summary>
|
||||||
/// <remarks> This is not 100% accurate due to world IDs, minions etc., but will be mostly sane. </remarks>
|
/// <remarks> This is not 100% accurate due to world IDs, minions etc., but will be mostly sane. </remarks>
|
||||||
private void RecoverGPoseActors()
|
private void RecoverGPoseActors()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue