From 5845b0f57d2aaabf2a837642a92a7a7c02ef9d69 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 2 Aug 2026 11:01:02 -0400 Subject: [PATCH 1/4] Add Finish vfunc for ExcelSheetWaiter I figured out one the vfuncs must be for when the waiter finishes, as a future waiter I'm adding invokes a callback here. --- ida/data.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/ida/data.yml b/ida/data.yml index b6dc5df09..24dd909c7 100644 --- a/ida/data.yml +++ b/ida/data.yml @@ -4291,6 +4291,7 @@ classes: 0x142084B50: Finalizer vfuncs: 0: dtor + 1: Finish Component::Excel::ExcelModuleInterface: vtbls: - ea: 0x14211ED90 From 3bbe27b9c7b08b32e297701b69cfc5d2ccf55fd4 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 2 Aug 2026 11:03:16 -0400 Subject: [PATCH 2/4] Add SpectatorManager and SpectatorSystem The names are guesswork, but their purpose is clear: managing the spectator system (at least the for CC Custom Matches.) The manager exists as a global and is initialized via an ActorControl, while the SpectatorSystem is held by Control and appears to do more of the grunt-work for the camera. --- .../FFXIV/Client/Game/Control/Control.cs | 9 +++ .../Client/Game/Control/SpectatorSystem.cs | 36 ++++++++++++ .../FFXIV/Client/Game/SpectatorManager.cs | 55 +++++++++++++++++++ ida/data.yml | 31 +++++++++++ 4 files changed, 131 insertions(+) create mode 100644 FFXIVClientStructs/FFXIV/Client/Game/Control/SpectatorSystem.cs create mode 100644 FFXIVClientStructs/FFXIV/Client/Game/SpectatorManager.cs diff --git a/FFXIVClientStructs/FFXIV/Client/Game/Control/Control.cs b/FFXIVClientStructs/FFXIV/Client/Game/Control/Control.cs index 4b06062bf..053506fd9 100644 --- a/FFXIVClientStructs/FFXIV/Client/Game/Control/Control.cs +++ b/FFXIVClientStructs/FFXIV/Client/Game/Control/Control.cs @@ -15,6 +15,7 @@ public unsafe partial struct Control { [FieldOffset(0x7518)] public bool IsWalkingDuringAutorun; [FieldOffset(0x7637)] public bool IsWalking; + [FieldOffset(0x7688)] public SpectatorSystem* SpectatorSystem; [FieldOffset(0x7698)] public uint LocalPlayerEntityId; [FieldOffset(0x76A0)] public BattleChara* LocalPlayer; [FieldOffset(0x76B0)] public Matrix4x4 ViewProjectionMatrix; @@ -40,4 +41,12 @@ public enum FlightAllowedStatus { DefaultCase = 7, PlayerOrMountNull = 8 } + + /// Creates a new SpectatorSystem. + [MemberFunction("E8 ?? ?? ?? ?? 33 DB 48 8B FD 66 90")] + public partial void CreateSpectatorControl(); + + /// Calls the destructor for SpectatorSystem and sets it back to null. + [MemberFunction("E8 ?? ?? ?? ?? 33 D2 48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B F0")] + public partial void FreeSpectatorControl(); } diff --git a/FFXIVClientStructs/FFXIV/Client/Game/Control/SpectatorSystem.cs b/FFXIVClientStructs/FFXIV/Client/Game/Control/SpectatorSystem.cs new file mode 100644 index 000000000..80d0f97df --- /dev/null +++ b/FFXIVClientStructs/FFXIV/Client/Game/Control/SpectatorSystem.cs @@ -0,0 +1,36 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Common.Math; + +namespace FFXIVClientStructs.FFXIV.Client.Game.Control; + +// Client::Game::Control::SpectatorSystem +[GenerateInterop] +[StructLayout(LayoutKind.Explicit, Size = 0xD0)] +public unsafe partial struct SpectatorSystem { + [FieldOffset(0x08), FixedSizeArray] internal FixedSizeArray8 _spectatorSpots; + [FieldOffset(0xAC)] public uint FocusObjectId; + [FieldOffset(0xB0)] public uint FocusSpotIndex; + [FieldOffset(0xB4)] public float FocusX; + [FieldOffset(0xB8)] public float FocusY; + [FieldOffset(0xBC)] public float FocusZ; + [FieldOffset(0xC0)] public float FocusDirH; + [FieldOffset(0xC4)] public float FocusDirV; + + /// Moves the spectator camera to a specific object. + [MemberFunction("E8 ?? ?? ?? ?? 48 8B 83 ?? ?? ?? ?? 45 85 FF")] + public partial void FocusOnObject(uint objectId); + + /// Moves the spectator camera to a spot at this index. + [MemberFunction("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 0F B6 86 ?? ?? ?? ?? 3B D8")] + public partial void FocusOnSpot(uint index, bool a3); + + [GenerateInterop] + [StructLayout(LayoutKind.Explicit, Size = 0x14)] + public unsafe partial struct SpectatorSpot { + [FieldOffset(0x00)] public float X; // Spectator.X + [FieldOffset(0x04)] public float Y; // Spectator.Y + [FieldOffset(0x08)] public float Z; // Spectator.Z + [FieldOffset(0x0C)] public float DirH; // Spectator.DirH + [FieldOffset(0x10)] public float DirV; // Spectator.DirV + } +} diff --git a/FFXIVClientStructs/FFXIV/Client/Game/SpectatorManager.cs b/FFXIVClientStructs/FFXIV/Client/Game/SpectatorManager.cs new file mode 100644 index 000000000..f28c13b0b --- /dev/null +++ b/FFXIVClientStructs/FFXIV/Client/Game/SpectatorManager.cs @@ -0,0 +1,55 @@ +using FFXIVClientStructs.FFXIV.Common.Component.Excel; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace FFXIVClientStructs.FFXIV.Client.Game; + +// Client::Game::SpectatorManager +/// +/// Initializes and manages the spectator system +/// +[GenerateInterop] +[StructLayout(LayoutKind.Explicit, Size = 0x4E0)] +public unsafe partial struct SpectatorManager { + [StaticAddress("4C 89 2D ?? ?? ?? ?? 45 33 C0", 3, isPointer: true)] + public static partial SpectatorManager* Instance(); + + [FieldOffset(0x008)] public ExcelSheet* Sheet; + [FieldOffset(0x010)] public SpectatorManagerWaiter* Waiter; + [FieldOffset(0x08C)] public uint SpectatorRowId; + [FieldOffset(0x094)] private uint Unk94; + [FieldOffset(0x098)] private uint Unk98; + [FieldOffset(0x090)] private byte Unk90; + [FieldOffset(0x0A1)] private byte Unknown40; // Set with Spectator.Unknown40 in ReadSpectatorRow + [FieldOffset(0x0A2)] public byte SheetStatus; // 0 = Missing, 1 = Sheet loaded, 2 = Row read + [FieldOffset(0x0DA)] private byte UnkDA; // 1, 2 and 100 (default?) are known flags. 2 shows the spectator management UI + + /// Initializes the SpectatorManager global. + [MemberFunction("48 89 5C 24 ?? 55 41 54 41 55 41 56 41 57 48 83 EC ?? 44 8B E1")] + public static partial void Initialize(uint spectatorRowId, uint a3, byte a4, uint a5); + + /// Updates and manages the Excel waiters here. + [MemberFunction("48 89 5C 24 ?? 56 48 83 EC ?? F3 0F 10 89")] + public partial void Update(); + + /// Reads spectator data from Excel, puts most of it into Control.SpectatorSystem. + [MemberFunction("48 8D 05 ?? ?? ?? ?? 48 89 47 ?? 48 89 5F ?? 48 89 77 ?? EB ?? 48 8B FE 44 8B 83")] + public partial void ReadSpectatorRow([CExporterExcel("Spectator")] void *spectatorRow); + + /// Changes an internal flag to tell the spectator UI to show itself. + [MemberFunction("40 53 48 83 EC ?? 80 A1 ?? ?? ?? ?? ?? 48 8B D9 E8 ?? ?? ?? ?? 80 8B")] + public partial void ShowSpectatorUI(); + + /// Used by AgentPvPSpectator to check if the regular PvP spectator UI should be visible. + [MemberFunction("E8 ?? ?? ?? ?? 84 C0 0F 84 ?? ?? ?? ?? 48 8B 83 ?? ?? ?? ?? 41 B0")] + public partial bool ShouldShowSpectatorUI(); + + [GenerateInterop] + [Inherits] + [StructLayout(LayoutKind.Explicit, Size = 0x50)] + public partial struct SpectatorManagerWaiter { + [FieldOffset(0x030)] private void* Unk30; + [FieldOffset(0x038)] public void* Arg; + [FieldOffset(0x040)] public void* Callback; + [FieldOffset(0x048)] private void* Unk48; + } +} diff --git a/ida/data.yml b/ida/data.yml index 24dd909c7..511fa4bcf 100644 --- a/ida/data.yml +++ b/ida/data.yml @@ -45,6 +45,7 @@ globals: 0x142A71554: g_animationFactorRemainder 0x142A71AB0: g_animationFactor #percentage of a 30fps frame rendered this frame 0x142A76110: g_PerformanceFrequency + 0x142A823E8: g_Client::Game::Control::Control_SpectatorSystem 0x142A823F8: g_Client::Game::Control::Control_LocalPlayerEntityId 0x142A82400: g_Client::Game::Control::Control_LocalPlayer 0x142A91500: g_NounPlaceholder_n @@ -627,6 +628,14 @@ classes: 0x141962EC0: ExecuteEmote # (emoteId, targetInfo) 0x141963780: CanExecuteEmote # (emoteId) 0x141964030: Update + Client::Game::Control::SpectatorSystem: + vtbls: + - ea: 0x142170A70 + vfuncs: + 0: dtor + funcs: + 0x14062D2F0: FocusOnObject + 0x14062D390: FocusOnSpot Application::Network::NetworkModule: funcs: 0x141CF2680: ctor @@ -954,6 +963,26 @@ classes: 0x141A7C840: GetAdjustedTimestamp # static 0x141A7C860: GetBonusGuaranteeForTimestamp # static 0x141A7C8A0: GetSatisfactionBonusesForNpc # static + Client::Game::SpectatorManager: + vtbls: + - ea: 0x1422CB8F8 + vfuncs: + 0: dtor + instances: + - ea: 0x142ACAC58 + pointer: true + funcs: + 0x141783290: Initialize + 0x141783490: GetInstance + 0x141785260: GetInstance2 + 0x141782A10: Update + 0x141782D60: ShowSpectatorUI + 0x1417834F0: ReadSpectatorRow + 0x141782C80: ShouldShowSpectatorUI + Client::Game::SpectatorManagerWaiter: + vtbls: + - ea: 0x1422CB988 + base: Common::Component::Excel::ExcelSheetWaiter Client::Game::InstanceContent::ContentSheetWaiterInterface: vtbls: - ea: 0x14218BBC8 @@ -8492,6 +8521,8 @@ classes: 0x14062C630: Initialize 0x14062C6C0: Terminate 0x14062C900: Update + 0x14062CEF0: CreateSpectatorControl + 0x14062CF70: FreeSpectatorControl 0x141832940: GetFlightAllowedStatus # static Client::Game::Control::CameraManager: funcs: From 1fd37ffa44a016a3a0d9e03859cfd9691e5b282e Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 2 Aug 2026 11:04:20 -0400 Subject: [PATCH 3/4] Add SpectatorList packet and handler This is used to fill the spectator list seen in CC Custom Matches accessible from the Duty Menu but also the PvP spectator menu. It's just a list of entity IDs, with an unknown but less important flag (probably used for counting and some other misc task.) --- .../FFXIV/Client/Game/Network/SpectatorListPacket.cs | 8 ++++++++ .../FFXIV/Client/Game/UI/InstanceContent.cs | 5 +++++ .../FFXIV/Client/Network/PacketDispatcher.cs | 3 +++ ida/data.yml | 2 ++ 4 files changed, 18 insertions(+) create mode 100644 FFXIVClientStructs/FFXIV/Client/Game/Network/SpectatorListPacket.cs diff --git a/FFXIVClientStructs/FFXIV/Client/Game/Network/SpectatorListPacket.cs b/FFXIVClientStructs/FFXIV/Client/Game/Network/SpectatorListPacket.cs new file mode 100644 index 000000000..b1a53ff44 --- /dev/null +++ b/FFXIVClientStructs/FFXIV/Client/Game/Network/SpectatorListPacket.cs @@ -0,0 +1,8 @@ +namespace FFXIVClientStructs.FFXIV.Client.Game.Network; + +[GenerateInterop] +[StructLayout(LayoutKind.Explicit, Size = 0x24)] +public partial struct SpectatorListPacket { + [FieldOffset(0x0), FixedSizeArray] internal FixedSizeArray8 _spectatorIds; + [FieldOffset(0x20)] private byte Flag; // Used as a count for spectator ids, maybe? +} diff --git a/FFXIVClientStructs/FFXIV/Client/Game/UI/InstanceContent.cs b/FFXIVClientStructs/FFXIV/Client/Game/UI/InstanceContent.cs index 114c655b4..3dc71080c 100644 --- a/FFXIVClientStructs/FFXIV/Client/Game/UI/InstanceContent.cs +++ b/FFXIVClientStructs/FFXIV/Client/Game/UI/InstanceContent.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.Game.Network; namespace FFXIVClientStructs.FFXIV.Client.Game.UI; @@ -10,6 +11,7 @@ public unsafe partial struct InstanceContent { [StaticAddress("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 84 C0 0F 94 C3", 3)] public static partial InstanceContent* Instance(); + [FieldOffset(0x38), FixedSizeArray] internal FixedSizeArray8 _spectatorIds; [FieldOffset(0x6C)] public byte RankedCrystallineConflictHostingDataCenterId; // saved as uint, used as byte? checks against 0xFF too [FieldOffset(0x70)] public bool IsLimitedTimeBonusActive; @@ -27,6 +29,9 @@ public unsafe partial struct InstanceContent { [MemberFunction("48 83 EC 28 84 D2 74 33")] public partial bool IsRouletteIncomplete(byte roulette); + [MemberFunction("8B 02 4C 8B C9 89 41")] + public partial void ProcessSpectatorListPacket(SpectatorListPacket* packet); + public bool IsRouletteComplete(byte roulette) => !IsRouletteIncomplete(roulette); [GenerateInterop] diff --git a/FFXIVClientStructs/FFXIV/Client/Network/PacketDispatcher.cs b/FFXIVClientStructs/FFXIV/Client/Network/PacketDispatcher.cs index 914a3dccb..116422320 100644 --- a/FFXIVClientStructs/FFXIV/Client/Network/PacketDispatcher.cs +++ b/FFXIVClientStructs/FFXIV/Client/Network/PacketDispatcher.cs @@ -96,6 +96,9 @@ public static partial void HandleActorControlPacket( [MemberFunction("E8 ?? ?? ?? ?? B0 ?? 48 8B 5C 24 ?? 48 8B 74 24 ?? 48 83 C4 ?? 5F C3 48 8B CB E8 ?? ?? ?? ?? B0 ?? 48 8B 5C 24 ?? 48 8B 74 24 ?? 48 83 C4 ?? 5F C3 48 8B 4B")] public static partial void HandleMapEffectPacket(MapEffectPacket* packet); + [MemberFunction("E8 ?? ?? ?? ?? B0 ?? 48 8B 5C 24 ?? 48 8B 74 24 ?? 48 83 C4 ?? 5F C3 48 8B CB E8 ?? ?? ?? ?? B0 ?? 48 8B 5C 24 ?? 48 8B 74 24 ?? 48 83 C4 ?? 5F C3 48 8B CB E8 ?? ?? ?? ?? 48 8B 5C 24")] + public static partial void HandleSpectatorListPacket(SpectatorListPacket* packet); + [VirtualFunction(1)] public partial void OnReceivePacket(uint targetId, nint packet); } diff --git a/ida/data.yml b/ida/data.yml index 511fa4bcf..83be63bc2 100644 --- a/ida/data.yml +++ b/ida/data.yml @@ -3630,6 +3630,7 @@ classes: - ea: 0x1421BA2B8 funcs: 0x140C27C10: GetPenaltyRemainingInMinutes + 0x140C28280: ProcessSpectatorListPacket 0x140C29720: GetContentInterface 0x140C2A5A0: IsRouletteIncomplete Client::Game::UI::GuildOrderReward: @@ -6585,6 +6586,7 @@ classes: 0x140B485E0: HandleDeepDungeonPartyInfoPacket 0x140B48620: HandleDeepDungeonChestInfoPacket 0x140B48660: HandleDeepDungeonBlacklistPacket + 0x140B48840: HandleSpectatorListPacket 0x140B497B0: HandlePrepareZoningPacket 0x140B49CB0: HandleMapEffectPacket 0x140B4A290: HandleQuestManagerUnkArray2SecondHalfPacket From bc2b1670440b7c5663d26a20ddd5e10a35ebecbe Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 2 Aug 2026 11:05:46 -0400 Subject: [PATCH 4/4] Add AgentPvPSpectator This is the agent responsible for the PvP spectator UI, and I really only focused on how it updated the visibility for its addons. --- .../Client/UI/Agent/AgentPvPSpectator.cs | 20 +++++++++++++++++++ ida/data.yml | 2 ++ 2 files changed, 22 insertions(+) create mode 100644 FFXIVClientStructs/FFXIV/Client/UI/Agent/AgentPvPSpectator.cs diff --git a/FFXIVClientStructs/FFXIV/Client/UI/Agent/AgentPvPSpectator.cs b/FFXIVClientStructs/FFXIV/Client/UI/Agent/AgentPvPSpectator.cs new file mode 100644 index 000000000..dbd8fdbf9 --- /dev/null +++ b/FFXIVClientStructs/FFXIV/Client/UI/Agent/AgentPvPSpectator.cs @@ -0,0 +1,20 @@ +namespace FFXIVClientStructs.FFXIV.Client.UI.Agent; + +// Client::UI::Agent::AgentPvPSpectator +// Client::UI::Agent::AgentInterface +// Component::GUI::AtkModuleInterface::AtkEventInterface +[Agent(AgentId.PvPSpectator)] +[GenerateInterop] +[Inherits] +[StructLayout(LayoutKind.Explicit, Size = 0xC0)] +public unsafe partial struct AgentPvPSpectator { + [FieldOffset(0x88)] public AgentPvPSpectatorData* Data; + + [MemberFunction("E8 ?? ?? ?? ?? 80 BB ?? ?? ?? ?? ?? 0F 84 ?? ?? ?? ?? 48 8B 4B ?? 48 89 AC 24")] + public partial void UpdateAddonVisibility(); +} + +[StructLayout(LayoutKind.Explicit, Size = 0x378)] +public unsafe partial struct AgentPvPSpectatorData { + [FieldOffset(0x354)] public uint AddonPvPSpectatorCameraList; +} diff --git a/ida/data.yml b/ida/data.yml index 83be63bc2..0fa96516f 100644 --- a/ida/data.yml +++ b/ida/data.yml @@ -14213,6 +14213,8 @@ classes: vtbls: - ea: 0x1421BFF58 base: Client::UI::Agent::AgentInterface + funcs: + 0x140D62EC0: UpdateAddonVisibility Client::UI::Agent::AgentAquarium: vtbls: - ea: 0x1421ED3B0