Skip to content

Commit 8779f4b

Browse files
committed
Add new cutscene ENPC tracking hooks.
1 parent 7b51739 commit 8779f4b

File tree

7 files changed

+152
-11
lines changed

7 files changed

+152
-11
lines changed

Penumbra.GameData

Penumbra/Interop/GameState.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ public class GameState : IService
1111
{
1212
#region Last Game Object
1313

14-
private readonly ThreadLocal<Queue<nint>> _lastGameObject = new(() => new Queue<nint>());
14+
private readonly ThreadLocal<Queue<nint>> _lastGameObject = new(() => new Queue<nint>());
15+
public readonly ThreadLocal<bool> CharacterAssociated = new(() => false);
1516

1617
public nint LastGameObject
1718
=> _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero;

Penumbra/Interop/Hooks/HookSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ public struct ObjectHooks
7676
public bool CreateCharacterBase;
7777
public bool EnableDraw;
7878
public bool WeaponReload;
79+
public bool SetupPlayerNpc;
80+
public bool ConstructCutsceneCharacter;
7981
}
8082

8183
public struct PostProcessingHooks
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using Dalamud.Hooking;
2+
using FFXIVClientStructs.FFXIV.Client.Game.Character;
3+
using OtterGui.Classes;
4+
using OtterGui.Services;
5+
using Penumbra.GameData;
6+
using Penumbra.GameData.Enums;
7+
using Penumbra.GameData.Interop;
8+
9+
namespace Penumbra.Interop.Hooks.Objects;
10+
11+
public sealed unsafe class ConstructCutsceneCharacter : EventWrapperPtr<Character, ConstructCutsceneCharacter.Priority>, IHookService
12+
{
13+
private readonly GameState _gameState;
14+
private readonly ObjectManager _objects;
15+
16+
public enum Priority
17+
{
18+
/// <seealso cref="PathResolving.CutsceneService.OnSetupPlayerNpc"/>
19+
CutsceneService = 0,
20+
}
21+
22+
public ConstructCutsceneCharacter(GameState gameState, HookManager hooks, ObjectManager objects)
23+
: base("ConstructCutsceneCharacter")
24+
{
25+
_gameState = gameState;
26+
_objects = objects;
27+
_task = hooks.CreateHook<Delegate>(Name, Sigs.ConstructCutsceneCharacter, Detour, !HookOverrides.Instance.Objects.ConstructCutsceneCharacter);
28+
}
29+
30+
private readonly Task<Hook<Delegate>> _task;
31+
32+
public delegate int Delegate(SetupPlayerNpc.SchedulerStruct* scheduler);
33+
34+
public int Detour(SetupPlayerNpc.SchedulerStruct* scheduler)
35+
{
36+
// This is the function that actually creates the new game object
37+
// and fills it into the object table at a free index etc.
38+
var ret = _task.Result.Original(scheduler);
39+
// Check for the copy state from SetupPlayerNpc.
40+
if (_gameState.CharacterAssociated.Value)
41+
{
42+
// If the newly created character exists, invoke the event.
43+
var character = _objects[ret + (int)ScreenActor.CutsceneStart].AsCharacter;
44+
if (character != null)
45+
{
46+
Invoke(character);
47+
Penumbra.Log.Verbose(
48+
$"[{Name}] Created indirect copy of player character at 0x{(nint)character}, index {character->ObjectIndex}.");
49+
}
50+
_gameState.CharacterAssociated.Value = false;
51+
}
52+
53+
return ret;
54+
}
55+
56+
public IntPtr Address
57+
=> _task.Result.Address;
58+
59+
public void Enable()
60+
=> _task.Result.Enable();
61+
62+
public void Disable()
63+
=> _task.Result.Disable();
64+
65+
public Task Awaiter
66+
=> _task;
67+
68+
public bool Finished
69+
=> _task.IsCompletedSuccessfully;
70+
}

Penumbra/Interop/Hooks/Objects/EnableDraw.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public EnableDraw(HookManager hooks, GameState state)
2626
private void Detour(GameObject* gameObject)
2727
{
2828
_state.QueueGameObject(gameObject);
29-
Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint)gameObject:X}.");
29+
Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint)gameObject:X} at {gameObject->ObjectIndex}.");
3030
_task.Result.Original.Invoke(gameObject);
3131
_state.DequeueGameObject();
3232
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using FFXIVClientStructs.FFXIV.Client.Game.Character;
2+
using OtterGui.Services;
3+
using Penumbra.GameData;
4+
5+
namespace Penumbra.Interop.Hooks.Objects;
6+
7+
public sealed unsafe class SetupPlayerNpc : FastHook<SetupPlayerNpc.Delegate>
8+
{
9+
private readonly GameState _gameState;
10+
11+
public SetupPlayerNpc(GameState gameState, HookManager hooks)
12+
{
13+
_gameState = gameState;
14+
Task = hooks.CreateHook<Delegate>("SetupPlayerNPC", Sigs.SetupPlayerNpc, Detour,
15+
!HookOverrides.Instance.Objects.SetupPlayerNpc);
16+
}
17+
18+
public delegate SchedulerStruct* Delegate(byte* npcType, nint unk, NpcSetupData* setupData);
19+
20+
public SchedulerStruct* Detour(byte* npcType, nint unk, NpcSetupData* setupData)
21+
{
22+
// This function actually seems to generate all NPC.
23+
24+
// If an ENPC is being created, check the creation parameters.
25+
// If CopyPlayerCustomize is true, the event NPC gets a timeline that copies its customize and glasses from the local player.
26+
// Keep track of this, so we can associate the actor to be created for this with the player character, see ConstructCutsceneCharacter.
27+
if (setupData->CopyPlayerCustomize && npcType != null && *npcType is 8)
28+
_gameState.CharacterAssociated.Value = true;
29+
30+
var ret = Task.Result.Original.Invoke(npcType, unk, setupData);
31+
Penumbra.Log.Excessive(
32+
$"[Setup Player NPC] Invoked for type {*npcType} with 0x{unk:X} and Copy Player Customize: {setupData->CopyPlayerCustomize}.");
33+
return ret;
34+
}
35+
36+
[StructLayout(LayoutKind.Explicit)]
37+
public struct NpcSetupData
38+
{
39+
[FieldOffset(0x0B)]
40+
private byte _copyPlayerCustomize;
41+
42+
public bool CopyPlayerCustomize
43+
{
44+
get => _copyPlayerCustomize != 0;
45+
set => _copyPlayerCustomize = value ? (byte)1 : (byte)0;
46+
}
47+
}
48+
49+
[StructLayout(LayoutKind.Explicit)]
50+
public struct SchedulerStruct
51+
{
52+
public static Character* GetCharacter(SchedulerStruct* s)
53+
=> ((delegate* unmanaged<SchedulerStruct*, Character*>**)s)[0][19](s);
54+
}
55+
}

Penumbra/Interop/PathResolving/CutsceneService.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,27 @@ public sealed class CutsceneService : IRequiredService, IDisposable
1515
public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd;
1616
public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx;
1717

18-
private readonly ObjectManager _objects;
19-
private readonly CopyCharacter _copyCharacter;
20-
private readonly CharacterDestructor _characterDestructor;
21-
private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray();
18+
private readonly ObjectManager _objects;
19+
private readonly CopyCharacter _copyCharacter;
20+
private readonly CharacterDestructor _characterDestructor;
21+
private readonly ConstructCutsceneCharacter _constructCutsceneCharacter;
22+
private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray();
2223

2324
public IEnumerable<KeyValuePair<int, IGameObject>> Actors
2425
=> Enumerable.Range(CutsceneStartIdx, CutsceneSlots)
2526
.Where(i => _objects[i].Valid)
2627
.Select(i => KeyValuePair.Create(i, this[i] ?? _objects.GetDalamudObject(i)!));
2728

2829
public unsafe CutsceneService(ObjectManager objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor,
29-
IClientState clientState)
30+
ConstructCutsceneCharacter constructCutsceneCharacter, IClientState clientState)
3031
{
31-
_objects = objects;
32-
_copyCharacter = copyCharacter;
33-
_characterDestructor = characterDestructor;
32+
_objects = objects;
33+
_copyCharacter = copyCharacter;
34+
_characterDestructor = characterDestructor;
35+
_constructCutsceneCharacter = constructCutsceneCharacter;
3436
_copyCharacter.Subscribe(OnCharacterCopy, CopyCharacter.Priority.CutsceneService);
3537
_characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.CutsceneService);
38+
_constructCutsceneCharacter.Subscribe(OnSetupPlayerNpc, ConstructCutsceneCharacter.Priority.CutsceneService);
3639
if (clientState.IsGPosing)
3740
RecoverGPoseActors();
3841
}
@@ -87,6 +90,7 @@ public unsafe void Dispose()
8790
{
8891
_copyCharacter.Unsubscribe(OnCharacterCopy);
8992
_characterDestructor.Unsubscribe(OnCharacterDestructor);
93+
_constructCutsceneCharacter.Unsubscribe(OnSetupPlayerNpc);
9094
}
9195

9296
private unsafe void OnCharacterDestructor(Character* character)
@@ -124,6 +128,15 @@ private unsafe void OnCharacterCopy(Character* target, Character* source)
124128
_copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1);
125129
}
126130

131+
private unsafe void OnSetupPlayerNpc(Character* npc)
132+
{
133+
if (npc == null || npc->ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx)
134+
return;
135+
136+
var idx = npc->GameObject.ObjectIndex - CutsceneStartIdx;
137+
_copiedCharacters[idx] = 0;
138+
}
139+
127140
/// <summary> Try to recover GPose actors on reloads into a running game. </summary>
128141
/// <remarks> This is not 100% accurate due to world IDs, minions etc., but will be mostly sane. </remarks>
129142
private void RecoverGPoseActors()

0 commit comments

Comments
 (0)