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/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/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/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 0444224e1..a1d179cb8 100644
--- a/FFXIVClientStructs/FFXIV/Client/Network/PacketDispatcher.cs
+++ b/FFXIVClientStructs/FFXIV/Client/Network/PacketDispatcher.cs
@@ -99,6 +99,9 @@ public static partial void HandleActorControlPacket(
[MemberFunction("4C 8D 89 ?? ?? ?? ?? 48 8B D1")]
public static partial void HandleAchievementsPacket(AchievementsPacket* 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/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 1e85bcab2..084a7683b 100644
--- a/ida/data.yml
+++ b/ida/data.yml
@@ -47,6 +47,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
@@ -629,6 +630,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
@@ -956,6 +965,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
@@ -3603,6 +3632,7 @@ classes:
- ea: 0x1421BA2B8
funcs:
0x140C27C10: GetPenaltyRemainingInMinutes
+ 0x140C28280: ProcessSpectatorListPacket
0x140C29720: GetContentInterface
0x140C2A5A0: IsRouletteIncomplete
Client::Game::UI::GuildOrderReward:
@@ -4293,6 +4323,7 @@ classes:
0x142084B50: Finalizer
vfuncs:
0: dtor
+ 1: Finish
Component::Excel::ExcelModuleInterface:
vtbls:
- ea: 0x14211ED90
@@ -6557,6 +6588,7 @@ classes:
0x140B485E0: HandleDeepDungeonPartyInfoPacket
0x140B48620: HandleDeepDungeonChestInfoPacket
0x140B48660: HandleDeepDungeonBlacklistPacket
+ 0x140B48840: HandleSpectatorListPacket
0x140B497B0: HandlePrepareZoningPacket
0x140B49CB0: HandleMapEffectPacket
0x140B4A290: HandleQuestManagerUnkArray2SecondHalfPacket
@@ -8493,6 +8525,8 @@ classes:
0x14062C630: Initialize
0x14062C6C0: Terminate
0x14062C900: Update
+ 0x14062CEF0: CreateSpectatorControl
+ 0x14062CF70: FreeSpectatorControl
0x141832940: GetFlightAllowedStatus # static
Client::Game::Control::CameraManager:
funcs:
@@ -14181,6 +14215,8 @@ classes:
vtbls:
- ea: 0x1421BFF58
base: Client::UI::Agent::AgentInterface
+ funcs:
+ 0x140D62EC0: UpdateAddonVisibility
Client::UI::Agent::AgentAquarium:
vtbls:
- ea: 0x1421ED3B0