diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000..73ed27df9f
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "FFXIVClientStructs"]
+ path = FFXIVClientStructs
+ url = https://github.com/awgil/FFXIVClientStructs.git
diff --git a/BossMod.sln b/BossMod.sln
index d92b3b648e..668192feec 100644
--- a/BossMod.sln
+++ b/BossMod.sln
@@ -9,24 +9,78 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UIDev", "UIDev\UIDev.csproj
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeAnalysis", "CodeAnalysis\CodeAnalysis.csproj", "{F5F7E565-DCF2-494A-A18F-F7FA503428F5}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs", "FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj", "{A1B9F2E8-1051-47C0-A480-20E1E9F8EF21}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.InteropSourceGenerators", "FFXIVClientStructs\FFXIVClientStructs.InteropSourceGenerators\FFXIVClientStructs.InteropSourceGenerators.csproj", "{0177B53D-6FFD-465D-A0A3-C18EA18BADD5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropGenerator", "FFXIVClientStructs\InteropGenerator\InteropGenerator.csproj", "{95FF6D8F-4513-470E-9C13-7FFC8A9D03B9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropGenerator.Runtime", "FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{6ED2F995-6CBE-4630-9BA6-4F46F1A84BCB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
+ Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|x64
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64
+ {9FFC3FB6-C9B3-4FA9-8378-F56B5916C8EC}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {9FFC3FB6-C9B3-4FA9-8378-F56B5916C8EC}.Debug|Any CPU.Build.0 = Debug|x64
{9FFC3FB6-C9B3-4FA9-8378-F56B5916C8EC}.Debug|x64.ActiveCfg = Debug|x64
{9FFC3FB6-C9B3-4FA9-8378-F56B5916C8EC}.Debug|x64.Build.0 = Debug|x64
+ {9FFC3FB6-C9B3-4FA9-8378-F56B5916C8EC}.Release|Any CPU.ActiveCfg = Release|x64
+ {9FFC3FB6-C9B3-4FA9-8378-F56B5916C8EC}.Release|Any CPU.Build.0 = Release|x64
{9FFC3FB6-C9B3-4FA9-8378-F56B5916C8EC}.Release|x64.ActiveCfg = Release|x64
{9FFC3FB6-C9B3-4FA9-8378-F56B5916C8EC}.Release|x64.Build.0 = Release|x64
+ {F5F7E565-DCF2-494A-A18F-F7FA503428F5}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {F5F7E565-DCF2-494A-A18F-F7FA503428F5}.Debug|Any CPU.Build.0 = Debug|x64
{F5F7E565-DCF2-494A-A18F-F7FA503428F5}.Debug|x64.ActiveCfg = Release|x64
{F5F7E565-DCF2-494A-A18F-F7FA503428F5}.Debug|x64.Build.0 = Release|x64
+ {F5F7E565-DCF2-494A-A18F-F7FA503428F5}.Release|Any CPU.ActiveCfg = Release|x64
+ {F5F7E565-DCF2-494A-A18F-F7FA503428F5}.Release|Any CPU.Build.0 = Release|x64
{F5F7E565-DCF2-494A-A18F-F7FA503428F5}.Release|x64.ActiveCfg = Release|x64
{F5F7E565-DCF2-494A-A18F-F7FA503428F5}.Release|x64.Build.0 = Release|x64
+ {A1B9F2E8-1051-47C0-A480-20E1E9F8EF21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1B9F2E8-1051-47C0-A480-20E1E9F8EF21}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1B9F2E8-1051-47C0-A480-20E1E9F8EF21}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A1B9F2E8-1051-47C0-A480-20E1E9F8EF21}.Debug|x64.Build.0 = Debug|Any CPU
+ {A1B9F2E8-1051-47C0-A480-20E1E9F8EF21}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1B9F2E8-1051-47C0-A480-20E1E9F8EF21}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A1B9F2E8-1051-47C0-A480-20E1E9F8EF21}.Release|x64.ActiveCfg = Release|Any CPU
+ {A1B9F2E8-1051-47C0-A480-20E1E9F8EF21}.Release|x64.Build.0 = Release|Any CPU
+ {0177B53D-6FFD-465D-A0A3-C18EA18BADD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0177B53D-6FFD-465D-A0A3-C18EA18BADD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0177B53D-6FFD-465D-A0A3-C18EA18BADD5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0177B53D-6FFD-465D-A0A3-C18EA18BADD5}.Debug|x64.Build.0 = Debug|Any CPU
+ {0177B53D-6FFD-465D-A0A3-C18EA18BADD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0177B53D-6FFD-465D-A0A3-C18EA18BADD5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0177B53D-6FFD-465D-A0A3-C18EA18BADD5}.Release|x64.ActiveCfg = Release|Any CPU
+ {0177B53D-6FFD-465D-A0A3-C18EA18BADD5}.Release|x64.Build.0 = Release|Any CPU
+ {95FF6D8F-4513-470E-9C13-7FFC8A9D03B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {95FF6D8F-4513-470E-9C13-7FFC8A9D03B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {95FF6D8F-4513-470E-9C13-7FFC8A9D03B9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {95FF6D8F-4513-470E-9C13-7FFC8A9D03B9}.Debug|x64.Build.0 = Debug|Any CPU
+ {95FF6D8F-4513-470E-9C13-7FFC8A9D03B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {95FF6D8F-4513-470E-9C13-7FFC8A9D03B9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {95FF6D8F-4513-470E-9C13-7FFC8A9D03B9}.Release|x64.ActiveCfg = Release|Any CPU
+ {95FF6D8F-4513-470E-9C13-7FFC8A9D03B9}.Release|x64.Build.0 = Release|Any CPU
+ {6ED2F995-6CBE-4630-9BA6-4F46F1A84BCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6ED2F995-6CBE-4630-9BA6-4F46F1A84BCB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6ED2F995-6CBE-4630-9BA6-4F46F1A84BCB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6ED2F995-6CBE-4630-9BA6-4F46F1A84BCB}.Debug|x64.Build.0 = Debug|Any CPU
+ {6ED2F995-6CBE-4630-9BA6-4F46F1A84BCB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6ED2F995-6CBE-4630-9BA6-4F46F1A84BCB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6ED2F995-6CBE-4630-9BA6-4F46F1A84BCB}.Release|x64.ActiveCfg = Release|Any CPU
+ {6ED2F995-6CBE-4630-9BA6-4F46F1A84BCB}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/BossMod/Autorotation/Autorotation.cs b/BossMod/Autorotation/Autorotation.cs
index 342288947f..b72d725660 100644
--- a/BossMod/Autorotation/Autorotation.cs
+++ b/BossMod/Autorotation/Autorotation.cs
@@ -96,7 +96,10 @@ public unsafe void Update()
if (Hints.ForcedTarget != null && PrimaryTarget != Hints.ForcedTarget)
{
PrimaryTarget = Hints.ForcedTarget;
- FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem.Instance()->Target = Utils.GameObjectInternal(Service.ObjectTable.FirstOrDefault(go => go.ObjectId == Hints.ForcedTarget.InstanceID));
+ var obj = Hints.ForcedTarget.SpawnIndex >= 0 ? FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager.Instance()->Objects.All[Hints.ForcedTarget.SpawnIndex].Value : null;
+ if (obj != null && obj->EntityId != Hints.ForcedTarget.InstanceID)
+ Service.Log($"[AR] Unexpected new target: expected {Hints.ForcedTarget.InstanceID:X} at #{Hints.ForcedTarget.SpawnIndex}, but found {obj->EntityId:X}");
+ FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem.Instance()->Target = obj;
}
Type? classType = null;
@@ -135,7 +138,10 @@ public unsafe void Update()
ClassActions?.FillStatusesToCancel(Hints.StatusesToCancel);
foreach (var s in Hints.StatusesToCancel)
- ActionManagerEx.Instance!.CancelStatus(s.statusId, s.sourceId != 0 ? (uint)s.sourceId : Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId);
+ {
+ var res = FFXIVClientStructs.FFXIV.Client.Game.StatusManager.ExecuteStatusOff(s.statusId, s.sourceId != 0 ? (uint)s.sourceId : Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId);
+ Service.Log($"[AR] Canceling status {s.statusId} from {s.sourceId:X} -> {res}");
+ }
_ui.IsOpen = ClassActions != null && Config.ShowUI;
diff --git a/BossMod/BossMod.csproj b/BossMod/BossMod.csproj
index dea5e9819b..b9a0d1ebb8 100644
--- a/BossMod/BossMod.csproj
+++ b/BossMod/BossMod.csproj
@@ -28,12 +28,6 @@
true
-
-
- PreserveNewest
-
-
-
@@ -58,10 +52,11 @@
false
Analyzer
-
+
+
$(DalamudLibPath)Newtonsoft.Json.dll
false
diff --git a/BossMod/Data/PartyState.cs b/BossMod/Data/PartyState.cs
index 09b4cc315e..8b92e84f5a 100644
--- a/BossMod/Data/PartyState.cs
+++ b/BossMod/Data/PartyState.cs
@@ -2,19 +2,21 @@
namespace BossMod;
-// state of the party/alliance that player is part of; part of the world state structure
+// state of the party/alliance/trust that player is part of; part of the world state structure
// solo player is considered to be in party of size 1
// after joining the party, member's slot never changes until leaving the party; this means that there could be intermediate gaps
// note that player could be in party without having actor in world (e.g. if he is in different zone)
// if player does not exist in world, party is always empty; otherwise player is always in slot 0
// in alliance, two 'other' groups use slots 8-15 and 16-23; alliance members don't have content-ID, but always have actor-ID
+// in trust, buddies are considered party members with content-id 0 (but non-zero actor id, they are always in world)
+// party slot is considered 'empty' if both ids are 0
public sealed class PartyState
{
public const int PlayerSlot = 0;
public const int MaxPartySize = 8;
public const int MaxAllianceSize = 24;
- private readonly ulong[] _contentIDs = new ulong[MaxPartySize]; // non-alliance slots: empty slots contain 0's, alliance slots: n/a (FF always reports 0)
+ private readonly ulong[] _contentIDs = new ulong[MaxPartySize]; // non-alliance slots: empty slots or buddy slots contain 0's, alliance slots: n/a (FF always reports 0)
private readonly ulong[] _actorIDs = new ulong[MaxAllianceSize]; // non-alliance slots: empty slots or slots corresponding to players not in world contain 0's, alliance slots: empty slots contains 0's
private readonly Actor?[] _actors = new Actor?[MaxAllianceSize];
diff --git a/BossMod/Debug/DebugAction.cs b/BossMod/Debug/DebugAction.cs
index 448d70122f..d5c3d3f72b 100644
--- a/BossMod/Debug/DebugAction.cs
+++ b/BossMod/Debug/DebugAction.cs
@@ -11,13 +11,19 @@ public unsafe void DrawActionManagerExtensions()
{
var am = ActionManagerEx.Instance!;
var amr = FFXIVClientStructs.FFXIV.Client.Game.ActionManager.Instance();
+ var aidCastAction = am.CastAction;
+ var aidCastSpell = am.CastSpell;
+ var aidCombo = new ActionID(ActionType.Spell, amr->Combo.Action);
+ var aidQueued = am.QueuedAction;
+ var aidGTAction = new ActionID((ActionType)amr->AreaTargetingActionType, amr->AreaTargetingActionId);
+ var aidGTSpell = new ActionID(ActionType.Spell, amr->AreaTargetingSpellId);
ImGui.TextUnformatted($"ActionManager singleton address: 0x{(ulong)amr:X}");
- ImGui.TextUnformatted($"Anim lock: {am.AnimationLock:f3}");
- ImGui.TextUnformatted($"Cast: {am.CastAction} / {am.CastSpell}, progress={am.CastTimeElapsed:f3}/{am.CastTimeTotal:f3}, target={am.CastTargetID:X}/{Utils.Vec3String(am.CastTargetPos)}");
- ImGui.TextUnformatted($"Combo: {new ActionID(ActionType.Spell, am.ComboLastMove)}, {am.ComboTimeLeft:f3}");
- ImGui.TextUnformatted($"Queue: {(am.QueueActive ? "active" : "inactive")}, {am.QueueAction} @ {am.QueueTargetID:X} [{am.QueueCallType}], combo={am.QueueComboRouteID}");
- ImGui.TextUnformatted($"GT: {am.GTAction} / {am.GTSpell}, arg={am.GTUnkArg}, obj={am.GTUnkObj:X}, a0={am.GTUnkA0:X2}, b8={am.GTUnkB8:X2}, bc={am.GTUnkBC:X}");
- ImGui.TextUnformatted($"Last used action sequence: {am.LastUsedActionSequence}");
+ ImGui.TextUnformatted($"Anim lock: {amr->AnimationLock:f3}");
+ ImGui.TextUnformatted($"Cast: {aidCastAction} / {aidCastSpell}, progress={amr->CastTimeElapsed:f3}/{amr->CastTimeTotal:f3}, target={amr->CastTargetId:X}/{Utils.Vec3String(amr->CastTargetPosition)}");
+ ImGui.TextUnformatted($"Combo: {aidCombo}, {am.ComboTimeLeft:f3}");
+ ImGui.TextUnformatted($"Queue: {(amr->ActionQueued ? "active" : "inactive")}, {aidQueued} @ {(ulong)amr->QueuedTargetId:X} [{amr->QueueType}], combo={amr->QueuedComboRouteId}");
+ ImGui.TextUnformatted($"GT: {aidGTAction} / {aidGTSpell}, arg={Utils.ReadField(amr, 0x94)}, obj={Utils.ReadField(amr, 0x98):X}, a0={Utils.ReadField(amr, 0xA0):X2}, b8={Utils.ReadField(amr, 0xB8):X2}, bc={Utils.ReadField(amr, 0xBC):X}");
+ ImGui.TextUnformatted($"Last used action sequence: {amr->LastUsedActionSequence}");
if (ImGui.Button("GT complete"))
{
Utils.WriteField(amr, 0xB8, (byte)1);
@@ -43,7 +49,7 @@ public unsafe void DrawActionData()
if (data != null)
{
ImGui.TextUnformatted($"Name: {data.Name}");
- ImGui.TextUnformatted($"Cast time: {data.Cast100ms / 10.0:f1}");
+ ImGui.TextUnformatted($"Cast time: {data.Cast100ms * 0.1f:f1} + {data.Unknown38 * 0.1f:f1}");
ImGui.TextUnformatted($"Range: {data.Range}");
ImGui.TextUnformatted($"Effect range: {data.EffectRange}");
ImGui.TextUnformatted($"Cooldown group: {data.CooldownGroup}");
@@ -110,7 +116,7 @@ public unsafe void DrawActionData()
ImGui.TextUnformatted($"Recast group: {groupID}");
var group = mgr->GetRecastGroupDetail(groupID);
if (group != null)
- ImGui.TextUnformatted($"Recast group details: active={group->IsActive}, action={group->ActionID}, elapsed={group->Elapsed:f3}, total={group->Total:f3}, cooldown={group->Total - group->Elapsed:f3}");
+ ImGui.TextUnformatted($"Recast group details: active={group->IsActive}, action={group->ActionId}, elapsed={group->Elapsed:f3}, total={group->Total:f3}, cooldown={group->Total - group->Elapsed:f3}");
}
}
else if (Service.GameGui.HoveredItem != 0)
@@ -130,7 +136,7 @@ public unsafe void DrawActionData()
ImGui.TextUnformatted($"Recast group: {groupID}");
var group = mgr->GetRecastGroupDetail(groupID);
if (group != null)
- ImGui.TextUnformatted($"Recast group details: active={group->IsActive}, action={group->ActionID}, elapsed={group->Elapsed}, total={group->Total}");
+ ImGui.TextUnformatted($"Recast group details: active={group->IsActive}, action={group->ActionId}, elapsed={group->Elapsed}, total={group->Total}");
}
else
{
diff --git a/BossMod/Debug/DebugAddon.cs b/BossMod/Debug/DebugAddon.cs
index 0d99b22d01..10d7898fb4 100644
--- a/BossMod/Debug/DebugAddon.cs
+++ b/BossMod/Debug/DebugAddon.cs
@@ -1,5 +1,4 @@
-using Dalamud.Hooking;
-using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
@@ -10,8 +9,8 @@ public unsafe sealed class DebugAddon : IDisposable
delegate nint AddonReceiveEventDelegate(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, ulong* inputData);
delegate void* AgentReceiveEventDelegate(AgentInterface* self, void* eventData, AtkValue* values, int valueCount, ulong eventKind);
- private readonly Dictionary> _rcvAddonHooks = [];
- private readonly Dictionary> _rcvAgentHooks = [];
+ private readonly Dictionary> _rcvAddonHooks = [];
+ private readonly Dictionary> _rcvAgentHooks = [];
private readonly Dictionary _addonRcvs = [];
private readonly Dictionary _agentRcvs = [];
private string _newHook = "";
@@ -34,26 +33,16 @@ public void Draw()
foreach (var (k, v) in _addonRcvs)
{
var hook = _rcvAddonHooks[v];
- if (ImGui.Button($"{(hook.IsEnabled ? "Disable" : "Enable")} {k} ({v:X})"))
- {
- if (hook.IsEnabled)
- hook.Disable();
- else
- hook.Enable();
- }
+ if (ImGui.Button($"{(hook.Enabled ? "Disable" : "Enable")} {k} ({v:X})"))
+ hook.Enabled ^= true;
}
ImGui.TextUnformatted("Agents:");
foreach (var (k, v) in _agentRcvs)
{
var hook = _rcvAgentHooks[v];
- if (ImGui.Button($"{(hook.IsEnabled ? "Disable" : "Enable")} {k} ({v:X})"))
- {
- if (hook.IsEnabled)
- hook.Disable();
- else
- hook.Enable();
- }
+ if (ImGui.Button($"{(hook.Enabled ? "Disable" : "Enable")} {k} ({v:X})"))
+ hook.Enabled ^= true;
}
ImGui.InputText("Addon name / agent id", ref _newHook, 256);
@@ -62,18 +51,17 @@ public void Draw()
ImGui.SameLine();
if (ImGui.Button("Hook addon!"))
{
- var address = (nint)addon->AtkEventListener.vfunc[2];
+ var address = (nint)addon->VirtualTable->ReceiveEvent;
_addonRcvs[_newHook] = address;
if (!_rcvAddonHooks.ContainsKey(address))
{
var name = _newHook;
- Hook hook = null!;
- _rcvAddonHooks[address] = hook = Service.Hook.HookFromAddress(address, (self, eventType, eventParam, eventData, inputData) =>
+ HookAddress hook = null!;
+ _rcvAddonHooks[address] = hook = new(address, (self, eventType, eventParam, eventData, inputData) =>
{
Service.Log($"RCV: listener={name} {(nint)self:X}, type={eventType}, param={eventParam}, input={inputData[0]:X16} {inputData[1]:X16} {inputData[2]:X16}");
return hook.Original(self, eventType, eventParam, eventData, inputData);
});
- hook.Enable();
}
}
}
@@ -82,17 +70,16 @@ public void Draw()
ImGui.SameLine();
if (ImGui.Button("Hook agent!"))
{
- var address = (nint)agent->VTable->ReceiveEvent;
+ var address = (nint)agent->VirtualTable->ReceiveEvent;
_agentRcvs[agentId] = address;
if (!_rcvAgentHooks.ContainsKey(address))
{
- Hook hook = null!;
- _rcvAgentHooks[address] = hook = Service.Hook.HookFromAddress(address, (self, eventData, values, valueCount, eventKind) =>
+ HookAddress hook = null!;
+ _rcvAgentHooks[address] = hook = new(address, (self, eventData, values, valueCount, eventKind) =>
{
Service.Log($"RCV: listener={agentId} {(nint)self:X}, kind={eventKind}, values={AtkValuesString(values, valueCount)}");
return hook.Original(self, eventData, values, valueCount, eventKind);
});
- hook.Enable();
}
}
}
@@ -115,8 +102,8 @@ private string AtkValuesString(AtkValue* values, int count)
FFXIVClientStructs.FFXIV.Component.GUI.ValueType.String8 => $"string8",
FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Vector => $"vector",
FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Texture => $"texture",
- FFXIVClientStructs.FFXIV.Component.GUI.ValueType.AllocatedString => $"astring",
- FFXIVClientStructs.FFXIV.Component.GUI.ValueType.AllocatedVector => $"avector",
+ FFXIVClientStructs.FFXIV.Component.GUI.ValueType.ManagedString => $"astring",
+ FFXIVClientStructs.FFXIV.Component.GUI.ValueType.ManagedVector => $"avector",
_ => $"{values[i].Type} unknown"
};
}
diff --git a/BossMod/Debug/DebugCollision.cs b/BossMod/Debug/DebugCollision.cs
deleted file mode 100644
index 84fdb0292a..0000000000
--- a/BossMod/Debug/DebugCollision.cs
+++ /dev/null
@@ -1,812 +0,0 @@
-using Dalamud.Memory;
-using Dalamud.Utility;
-using FFXIVClientStructs.FFXIV.Client.System.Framework;
-using ImGuiNET;
-using System.Runtime.InteropServices;
-using System.Text;
-
-namespace BossMod;
-
-[StructLayout(LayoutKind.Explicit, Size = 0x30)]
-public unsafe struct Mat4x3
-{
- [FieldOffset(0x00)] public Vector3 Row0;
- [FieldOffset(0x0C)] public Vector3 Row1;
- [FieldOffset(0x18)] public Vector3 Row2;
- [FieldOffset(0x24)] public Vector3 Row3;
-
- public readonly SharpDX.Matrix M => new(Row0.X, Row0.Y, Row0.Z, 0, Row1.X, Row1.Y, Row1.Z, 0, Row2.X, Row2.Y, Row2.Z, 0, Row3.X, Row3.Y, Row3.Z, 1);
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0x20)]
-public unsafe struct CollisionNode
-{
- [FieldOffset(0x00)] public void** Vtbl;
- [FieldOffset(0x08)] public void** Vtbl8;
- [FieldOffset(0x10)] public CollisionNode* Prev;
- [FieldOffset(0x18)] public CollisionNode* Next;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0xC0)]
-public unsafe struct CollisionModule
-{
- [FieldOffset(0x10)] public CollisionSceneManager* Manager;
- [FieldOffset(0xA8)] public int LoadInProgressCounter;
- [FieldOffset(0xAC)] public Vector4 ForcedStreamingBounds;
-
- public static CollisionModule* Instance => (CollisionModule*)Framework.Instance()->BGCollisionModule;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0x38)]
-public unsafe struct CollisionSceneManager
-{
- [FieldOffset(0x00)] public void** Vtbl;
- [FieldOffset(0x18)] public CollisionSceneWrapper* FirstScene;
- [FieldOffset(0x20)] public int NumScenes;
- [FieldOffset(0x28)] public Vector4 StreamingBounds;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0x30)]
-public unsafe struct CollisionSceneWrapper
-{
- [FieldOffset(0x00)] public CollisionNode Base;
- [FieldOffset(0x20)] public CollisionSceneManager* Owner;
- [FieldOffset(0x28)] public CollisionScene* Scene;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0x40)]
-public unsafe struct CollisionScene
-{
- [FieldOffset(0x00)] public void** Vtbl;
- [FieldOffset(0x08)] public CollisionSceneManager* Owner;
- [FieldOffset(0x10)] public CollisionObjectBase* FirstObj;
- [FieldOffset(0x18)] public int NumObjs;
- [FieldOffset(0x20)] public Vector4 StreamingBounds; // center = player pos, w = radius
- [FieldOffset(0x30)] public int NumLoading;
- [FieldOffset(0x38)] public CollisionQuadtree* Quadtree;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0x40)]
-public unsafe struct CollisionQuadtree
-{
- [FieldOffset(0x00)] public void** Vtbl;
- [FieldOffset(0x08)] public float MinX;
- [FieldOffset(0x0C)] public float MaxX;
- [FieldOffset(0x10)] public float LeafSizeX;
- [FieldOffset(0x14)] public float MinZ;
- [FieldOffset(0x18)] public float MaxZ;
- [FieldOffset(0x1C)] public float LeafSizeZ;
- [FieldOffset(0x20)] public int NumLevels;
- [FieldOffset(0x28)] public CollisionNode* Nodes;
- [FieldOffset(0x30)] public int NumNodes;
- [FieldOffset(0x38)] public void* Owner;
-}
-
-public enum CollisionObjectType : int
-{
- Multi = 1,
- Shape = 2,
- Box = 3,
- Cylinder = 4,
- Sphere = 5,
- Plane = 6,
- PlaneTwoSided = 7,
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0xA0)]
-public unsafe struct CollisionObjectBase
-{
- [FieldOffset(0x00)] public CollisionObjectBaseVTable* Vtbl;
- [FieldOffset(0x00)] public CollisionNode Base;
- [FieldOffset(0x30)] public CollisionObjectBase* PrevNodeObj;
- [FieldOffset(0x38)] public CollisionObjectBase* NextNodeObj;
- [FieldOffset(0x44)] public uint NumRefs;
- [FieldOffset(0x48)] public CollisionScene* Scene;
- [FieldOffset(0x68)] public uint LayerMask;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 24 * 8)]
-public unsafe struct CollisionObjectBaseVTable
-{
- [FieldOffset(17 * 8)] public delegate* unmanaged[Stdcall] GetObjectType;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0x20)]
-public unsafe struct CollisionObjectMultiElement
-{
- [FieldOffset(0x00)] public int SubFile;
- [FieldOffset(0x08)] public CollisionObjectShape* Shape;
- [FieldOffset(0x10)] public float MinX;
- [FieldOffset(0x14)] public float MinZ;
- [FieldOffset(0x18)] public float MaxX;
- [FieldOffset(0x1C)] public float MaxZ;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0x1E0)]
-public unsafe struct CollisionObjectMulti // type 1
-{
- [FieldOffset(0x000)] public CollisionObjectBase Base;
- [FieldOffset(0x0A8)] public fixed byte PathBase[256];
- [FieldOffset(0x1B8)] public float StreamedMinX;
- [FieldOffset(0x1BC)] public float StreamedMinZ;
- [FieldOffset(0x1C0)] public float StreamedMaxX;
- [FieldOffset(0x1C4)] public float StreamedMaxZ;
- [FieldOffset(0x1C8)] public int* PtrNumElements;
- [FieldOffset(0x1D8)] public CollisionObjectMultiElement* Elements;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0x198)]
-public unsafe struct CollisionObjectShape // type 2
-{
- [FieldOffset(0x000)] public CollisionObjectBase Base;
- [FieldOffset(0x0C8)] public CollisionShapePCB* Shape; // pointer to interface really
- [FieldOffset(0x0D4)] public Vector3 Translation;
- [FieldOffset(0x0E0)] public Vector3 Rotation;
- [FieldOffset(0x0EC)] public Vector3 Scale;
- [FieldOffset(0x0F8)] public Vector3 TranslationPrev;
- [FieldOffset(0x104)] public Vector3 RotationPrev;
- [FieldOffset(0x110)] public Mat4x3 World;
- [FieldOffset(0x140)] public Mat4x3 InvWorld;
- [FieldOffset(0x170)] public Vector4 BoundingSphere;
- [FieldOffset(0x180)] public Vector3 BoundingBoxMin;
- [FieldOffset(0x18C)] public Vector3 BoundingBoxMax;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0x140)]
-public unsafe struct CollisionObjectBox // type 3
-{
- [FieldOffset(0x000)] public CollisionObjectBase Base;
- [FieldOffset(0x0A0)] public Vector3 Translation;
- [FieldOffset(0x0AC)] public Vector3 TranslationPrev;
- [FieldOffset(0x0B8)] public Vector3 Rotation;
- [FieldOffset(0x0C4)] public Vector3 RotationPrev;
- [FieldOffset(0x0D0)] public Vector3 Scale;
- [FieldOffset(0x0DC)] public Mat4x3 World;
- [FieldOffset(0x10C)] public Mat4x3 InvWorld;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0x148)]
-public unsafe struct CollisionObjectCylinder // type 4
-{
- [FieldOffset(0x000)] public CollisionObjectBase Base;
- [FieldOffset(0x0A0)] public Vector3 Translation;
- [FieldOffset(0x0AC)] public Vector3 TranslationPrev;
- [FieldOffset(0x0B8)] public Vector3 Rotation;
- [FieldOffset(0x0C4)] public Vector3 RotationPrev;
- [FieldOffset(0x0D0)] public Vector3 Scale;
- [FieldOffset(0x0DC)] public float Radius;
- [FieldOffset(0x0E0)] public Mat4x3 World;
- [FieldOffset(0x110)] public Mat4x3 InvWorld;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0x150)]
-public unsafe struct CollisionObjectSphere // type 5
-{
- [FieldOffset(0x000)] public CollisionObjectBase Base;
- [FieldOffset(0x0A4)] public Vector3 Translation;
- [FieldOffset(0x0B0)] public Vector3 TranslationPrev;
- [FieldOffset(0x0BC)] public Vector3 Rotation;
- [FieldOffset(0x0C8)] public Vector3 RotationPrev;
- [FieldOffset(0x0D4)] public Vector3 Scale;
- [FieldOffset(0x0E0)] public Vector3 ScalePrev;
- [FieldOffset(0x0EC)] public Mat4x3 World;
- [FieldOffset(0x11C)] public Mat4x3 InvWorld;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0x140)]
-public unsafe struct CollisionObjectPlane // type 6/7
-{
- [FieldOffset(0x000)] public CollisionObjectBase Base;
- [FieldOffset(0x0A0)] public Vector3 Translation;
- [FieldOffset(0x0AC)] public Vector3 TranslationPrev;
- [FieldOffset(0x0B8)] public Vector3 Rotation;
- [FieldOffset(0x0C4)] public Vector3 RotationPrev;
- [FieldOffset(0x0D0)] public Vector3 Scale;
- [FieldOffset(0x0DC)] public Mat4x3 World;
- [FieldOffset(0x10C)] public Mat4x3 InvWorld;
- [FieldOffset(0x13D)] public byte Extended;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 0x40)]
-public unsafe struct CollisionShapePCB
-{
- [FieldOffset(0x00)] public void** Vtbl;
- [FieldOffset(0x08)] public void** Vtbl8;
- [FieldOffset(0x10)] public CollisionObjectShape* OwnerObj;
- [FieldOffset(0x18)] public CollisionShapePCBData* Data;
-};
-
-[StructLayout(LayoutKind.Explicit, Size = 0x30)] // variable length structure: followed by raw verts then compressed verts then prims
-public unsafe struct CollisionShapePCBData
-{
- [FieldOffset(0x00)] public ulong Header;
- [FieldOffset(0x08)] public int Child1Offset;
- [FieldOffset(0x0C)] public int Child2Offset;
- [FieldOffset(0x10)] public Vector3 AABBMin;
- [FieldOffset(0x1C)] public Vector3 AABBMax;
- [FieldOffset(0x28)] public ushort NumVertsCompressed; // ushort[3] per vert
- [FieldOffset(0x2A)] public ushort NumPrims;
- [FieldOffset(0x2C)] public ushort NumVertsRaw; // vector3 per vert
-};
-
-[StructLayout(LayoutKind.Explicit, Size = 0xC)]
-public unsafe struct CollisionShapePrimitive
-{
- [FieldOffset(0x0)] public byte V1;
- [FieldOffset(0x1)] public byte V2;
- [FieldOffset(0x2)] public byte V3;
- [FieldOffset(0x4)] public uint Flags;
- [FieldOffset(0x8)] public uint Unk8;
-}
-
-public unsafe sealed class DebugCollision : IDisposable
-{
- private readonly UITree _tree = new();
- private (Action, nint) _drawExtra;
- private readonly HashSet _slaveShapes = [];
-
- private readonly nint _typeinfoCollisionShapePCB;
-
- public DebugCollision()
- {
- _typeinfoCollisionShapePCB = Service.SigScanner.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? 48 89 78 10 48 89 08");
- Service.Log($"vtbl CollisionShapePCB: {_typeinfoCollisionShapePCB:X}");
-
- _drawExtra = (() => { }, 0);
- }
-
- public void Dispose()
- {
-
- }
-
- public void Draw()
- {
- UpdateSlaveShapes();
-
- if (ImGui.Button("Clear selection"))
- _drawExtra = (() => { }, 0);
- ImGui.SameLine();
- if (ImGui.Button("Export to obj"))
- ExportToObj(true, true);
- ImGui.SameLine();
- if (ImGui.Button($"Generate report"))
- Report();
-
- //var screenPos = ImGui.GetMousePos();
- //var ray = CameraManager.Instance()->CurrentCamera->ScreenPointToRay(screenPos);
- //BGCollisionModule.Raycast(ray.Origin, ray.Direction, out var hitInfo);
- //ImGui.TextUnformatted($"S2W: {screenPos} -> {hitInfo.Point}");
- //for (int i = 0; i < 0x58; i += 8)
- // ImGui.TextUnformatted($"{i:X} = {Utils.ReadField(&hitInfo, i):X16}");
-
- var module = CollisionModule.Instance;
- ImGui.TextUnformatted($"Module: {(nint)module:X}->{(nint)module->Manager:X} ({module->Manager->NumScenes} scenes, {module->LoadInProgressCounter} loads)");
- ImGui.TextUnformatted($"Streaming: {SphereStr(module->ForcedStreamingBounds)} / {SphereStr(module->Manager->StreamingBounds)}");
-
- var scene = module->Manager->FirstScene;
- while (scene != null)
- {
- DrawScene(scene);
- scene = (CollisionSceneWrapper*)scene->Base.Next;
- }
-
- _drawExtra.Item1();
- }
-
- private void UpdateSlaveShapes()
- {
- bool foundCtx = _drawExtra.Item2 == 0;
-
- _slaveShapes.Clear();
- var scene = CollisionModule.Instance->Manager->FirstScene;
- while (scene != null)
- {
- var obj = scene->Scene->FirstObj;
- while (obj != null)
- {
- foundCtx |= (nint)obj == _drawExtra.Item2;
- if (obj->Vtbl->GetObjectType(obj) == CollisionObjectType.Multi)
- {
- var castObj = (CollisionObjectMulti*)obj;
- if (castObj->Elements != null)
- {
- for (int i = 0; i < *castObj->PtrNumElements; ++i)
- _slaveShapes.Add((nint)castObj->Elements[i].Shape);
- }
- }
- obj = (CollisionObjectBase*)obj->Base.Next;
- }
- scene = (CollisionSceneWrapper*)scene->Base.Next;
- }
-
- if (!foundCtx)
- {
- Service.Log($"resetting selection for {_drawExtra.Item2:X}");
- _drawExtra = (() => { }, 0);
- }
- }
-
- private void DrawScene(CollisionSceneWrapper* wrapper)
- {
- foreach (var n in _tree.Node($"{(nint)wrapper:X}->{(nint)wrapper->Scene:X} ({wrapper->Scene->NumObjs} objects, {wrapper->Scene->NumLoading} loads)###{(nint)wrapper:X}", select: MakeSelect(() => VisualizeScene(wrapper->Scene), null)))
- {
- _tree.LeafNode($"Streaming bounds: [{SphereStr(wrapper->Scene->StreamingBounds)}");
-
- foreach (var n2 in _tree.Node($"Objects"))
- {
- var obj = wrapper->Scene->FirstObj;
- while (obj != null)
- {
- DrawObject(obj);
- obj = (CollisionObjectBase*)obj->Base.Next;
- }
- }
-
- var tree = wrapper->Scene->Quadtree;
- foreach (var n2 in _tree.Node($"Quadtree {(nint)tree:X}: {tree->NumLevels} levels ([{tree->MinX}, {tree->MaxX}]x[{tree->MinZ}, {tree->MaxZ}], leaf {tree->LeafSizeX}x{tree->LeafSizeZ}), {tree->NumNodes} nodes", tree->NumNodes == 0))
- {
- for (int i = 0; i < tree->NumNodes; ++i)
- {
- var node = tree->Nodes + i;
- foreach (var n3 in _tree.Node($"{i}", node->Next == null, select: MakeSelect(() => VisualizeQuadtreeNode(node), null)))
- {
- var child = (CollisionObjectBase*)node->Next;
- while (child != null && child != node)
- {
- DrawObject(child);
- child = child->NextNodeObj;
- }
- }
- }
- }
- }
- }
-
- private void DrawObject(CollisionObjectBase* obj)
- {
- var type = obj->Vtbl->GetObjectType(obj);
- foreach (var n3 in _tree.Node($"{type} {(nint)obj:X}, layers={obj->LayerMask:X8}, refs={obj->NumRefs}", color: _slaveShapes.Contains((nint)obj) ? ArenaColor.Safe : 0xffffffff, select: MakeSelect(() => VisualizeObject(obj), obj)))
- {
- switch (type)
- {
- case CollisionObjectType.Multi:
- {
- var castObj = (CollisionObjectMulti*)obj;
- var path = MemoryHelper.ReadStringNullTerminated((nint)castObj->PathBase);
- _tree.LeafNode($"Path: {path}");
- _tree.LeafNode($"Filename: {MemoryHelper.ReadStringNullTerminated((nint)castObj->PathBase + path.Length + 1)}");
- _tree.LeafNode($"Streamed: [{castObj->StreamedMinX:f3}x{castObj->StreamedMinZ:f3}] - [{castObj->StreamedMaxX:f3}x{castObj->StreamedMaxZ:f3}]");
- if (castObj->Elements != null)
- {
- foreach (var n4 in _tree.Node($"Elements: {*castObj->PtrNumElements}"))
- {
- for (int i = 0; i < *castObj->PtrNumElements; ++i)
- {
- var elem = castObj->Elements + i;
- var elemBase = &elem->Shape->Base;
- foreach (var n5 in _tree.Node($"#{i}: {elem->SubFile:d4} [{elem->MinX:f3}x{elem->MinZ:f3}] - [{elem->MaxX:f3}x{elem->MaxZ:f3}] == {(nint)elem->Shape:X}", elem->Shape == null, select: MakeSelect(() => VisualizeObject(elemBase), elemBase)))
- if (elem->Shape != null)
- DrawObjectShape(elem->Shape);
- }
- }
- }
- }
- break;
- case CollisionObjectType.Shape:
- DrawObjectShape((CollisionObjectShape*)obj);
- break;
- case CollisionObjectType.Box:
- {
- var castObj = (CollisionObjectBox*)obj;
- _tree.LeafNode($"Translation: {Utils.Vec3String(castObj->Translation)}");
- _tree.LeafNode($"Rotation: {Utils.Vec3String(castObj->Rotation)}");
- _tree.LeafNode($"Scale: {Utils.Vec3String(castObj->Scale)}");
- DrawMat4x3("World", ref castObj->World);
- DrawMat4x3("InvWorld", ref castObj->InvWorld);
- }
- break;
- case CollisionObjectType.Cylinder:
- {
- var castObj = (CollisionObjectCylinder*)obj;
- _tree.LeafNode($"Translation: {Utils.Vec3String(castObj->Translation)}");
- _tree.LeafNode($"Rotation: {Utils.Vec3String(castObj->Rotation)}");
- _tree.LeafNode($"Scale: {Utils.Vec3String(castObj->Scale)}");
- _tree.LeafNode($"Radius: {castObj->Radius:f3}");
- DrawMat4x3("World", ref castObj->World);
- DrawMat4x3("InvWorld", ref castObj->InvWorld);
- }
- break;
- case CollisionObjectType.Sphere:
- {
- var castObj = (CollisionObjectSphere*)obj;
- _tree.LeafNode($"Translation: {Utils.Vec3String(castObj->Translation)}");
- _tree.LeafNode($"Rotation: {Utils.Vec3String(castObj->Rotation)}");
- _tree.LeafNode($"Scale: {Utils.Vec3String(castObj->Scale)}");
- DrawMat4x3("World", ref castObj->World);
- DrawMat4x3("InvWorld", ref castObj->InvWorld);
- }
- break;
- case CollisionObjectType.Plane:
- case CollisionObjectType.PlaneTwoSided:
- {
- var castObj = (CollisionObjectPlane*)obj;
- _tree.LeafNode($"Translation: {Utils.Vec3String(castObj->Translation)}");
- _tree.LeafNode($"Rotation: {Utils.Vec3String(castObj->Rotation)}");
- _tree.LeafNode($"Scale: {Utils.Vec3String(castObj->Scale)}");
- DrawMat4x3("World", ref castObj->World);
- DrawMat4x3("InvWorld", ref castObj->InvWorld);
- }
- break;
- }
- }
- }
-
- private void DrawObjectShape(CollisionObjectShape* obj)
- {
- _tree.LeafNode($"Translation: {Utils.Vec3String(obj->Translation)}");
- _tree.LeafNode($"Rotation: {Utils.Vec3String(obj->Rotation)}");
- _tree.LeafNode($"Scale: {Utils.Vec3String(obj->Scale)}");
- DrawMat4x3("World", ref obj->World);
- DrawMat4x3("InvWorld", ref obj->InvWorld);
- _tree.LeafNode($"Bounding sphere: [{SphereStr(obj->BoundingSphere)}", select: MakeSelect(() => VisualizeSphere(obj->BoundingSphere), &obj->Base));
- _tree.LeafNode($"Bounding box: {Utils.Vec3String(obj->BoundingBoxMin)} - {Utils.Vec3String(obj->BoundingBoxMax)}", select: MakeSelect(() => VisualizeAABB(obj->BoundingBoxMin, obj->BoundingBoxMax), &obj->Base));
-
- bool shapeHasData = obj->Shape != null && (nint)obj->Shape->Vtbl == _typeinfoCollisionShapePCB && obj->Shape->Data != null;
- var shapeType = obj->Shape == null ? "null" : (nint)obj->Shape->Vtbl == _typeinfoCollisionShapePCB ? "PCB" : $"unknown +{(nint)obj->Shape->Vtbl - Service.SigScanner.Module.BaseAddress:X}";
- foreach (var n in _tree.Node($"Shape: {(nint)obj->Shape:X} {shapeType}", !shapeHasData, select: MakeSelect(shapeHasData ? () => VisualizeShape(obj->Shape->Data, obj) : () => { }, &obj->Base)))
- {
- if (shapeHasData)
- DrawPCBShape(obj->Shape->Data, obj);
- }
- }
-
- private void DrawPCBShape(CollisionShapePCBData* data, CollisionObjectShape* obj)
- {
- if (data == null)
- return;
- _tree.LeafNode($"Header: {data->Header:X16}");
- _tree.LeafNode($"AABB: {Utils.Vec3String(data->AABBMin)} - {Utils.Vec3String(data->AABBMax)}", select: MakeSelect(() => VisualizeOBB(data->AABBMin, data->AABBMax, obj), &obj->Base));
- foreach (var n in _tree.Node($"Vertices: {data->NumVertsRaw}+{data->NumVertsCompressed}", data->NumVertsRaw + data->NumVertsCompressed == 0))
- {
- var pRaw = (float*)(data + 1);
- for (int i = 0; i < data->NumVertsRaw; ++i)
- {
- var v = new Vector3(pRaw[0], pRaw[1], pRaw[2]);
- _tree.LeafNode($"[{i}] (r): {Utils.Vec3String(v)}", select: MakeSelect(() => VisualizeVertex(v, obj), &obj->Base));
- pRaw += 3;
- }
- var pCompressed = (ushort*)pRaw;
- var quantScale = (data->AABBMax - data->AABBMin) / 65535.0f;
- for (int i = 0; i < data->NumVertsCompressed; ++i)
- {
- var v = data->AABBMin + quantScale * new Vector3(pCompressed[0], pCompressed[1], pCompressed[2]);
- _tree.LeafNode($"[{i + data->NumVertsRaw}] (c): {Utils.Vec3String(v)}", select: MakeSelect(() => VisualizeVertex(v, obj), &obj->Base));
- pCompressed += 3;
- }
- }
- foreach (var n in _tree.Node($"Primitives: {data->NumPrims}", data->NumPrims == 0))
- {
- var pRaw = (float*)(data + 1);
- var pCompr = (ushort*)(pRaw + 3 * data->NumVertsRaw);
- var pPrims = (CollisionShapePrimitive*)(pCompr + 3 * data->NumVertsCompressed);
- for (int i = 0; i < data->NumPrims; ++i)
- {
- var idx = i;
- _tree.LeafNode($"[{i}]: {pPrims->V1}x{pPrims->V2}x{pPrims->V3}, {pPrims->Flags:X8}, {pPrims->Unk8:X8}", select: MakeSelect(() => VisualizePrimitive(data, idx, obj), &obj->Base));
- ++pPrims;
- }
- }
- foreach (var n in _tree.Node($"Child 1 (+{data->Child1Offset})", data->Child1Offset == 0, select: MakeSelect(data->Child1Offset != 0 ? () => VisualizeShape((CollisionShapePCBData*)((byte*)data + data->Child1Offset), obj) : () => { }, &obj->Base)))
- {
- if (data->Child1Offset != 0)
- DrawPCBShape((CollisionShapePCBData*)((byte*)data + data->Child1Offset), obj);
- }
- foreach (var n in _tree.Node($"Child 2 (+{data->Child2Offset})", data->Child2Offset == 0, select: MakeSelect(data->Child2Offset != 0 ? () => VisualizeShape((CollisionShapePCBData*)((byte*)data + data->Child2Offset), obj) : () => { }, &obj->Base)))
- {
- if (data->Child2Offset != 0)
- DrawPCBShape((CollisionShapePCBData*)((byte*)data + data->Child2Offset), obj);
- }
- }
-
- private void DrawMat4x3(string tag, ref Mat4x3 mat)
- {
- _tree.LeafNode($"{tag} R0: {Utils.Vec3String(mat.Row0)}");
- _tree.LeafNode($"{tag} R1: {Utils.Vec3String(mat.Row1)}");
- _tree.LeafNode($"{tag} R2: {Utils.Vec3String(mat.Row2)}");
- _tree.LeafNode($"{tag} R3: {Utils.Vec3String(mat.Row3)}");
- }
-
- private Action MakeSelect(Action sel, CollisionObjectBase* ctx) => () =>
- {
- Service.Log($"select: {(nint)ctx:X}");
- _drawExtra = (sel, (nint)ctx);
- };
-
- private void VisualizeSphere(Vector4 sphere) => Camera.Instance?.DrawWorldSphere(new(sphere.X, sphere.Y, sphere.Z), sphere.W, ArenaColor.Safe);
- private void VisualizeAABB(Vector3 min, Vector3 max) => Camera.Instance?.DrawWorldOBB(min, max, SharpDX.Matrix.Identity, ArenaColor.Safe);
- private void VisualizeOBB(Vector3 min, Vector3 max, CollisionObjectShape* obj) => Camera.Instance?.DrawWorldOBB(min, max, obj->World.M, ArenaColor.Safe);
- private void VisualizeVertex(Vector3 v, CollisionObjectShape* obj) => Camera.Instance?.DrawWorldSphere(SharpDX.Vector3.TransformCoordinate(new(v.X, v.Y, v.Z), obj->World.M).ToSystem(), 0.1f, ArenaColor.Danger);
-
- private void VisualizePrimitive(CollisionShapePCBData* data, int iPrim, CollisionObjectShape* obj, uint color = ArenaColor.Danger)
- {
- var pRaw = (float*)(data + 1);
- var pCompr = (ushort*)(pRaw + 3 * data->NumVertsRaw);
- var pPrim = (CollisionShapePrimitive*)(pCompr + 3 * data->NumVertsCompressed);
- pPrim += iPrim;
- var v1 = LocalVertex(data, pPrim->V1);
- var v2 = LocalVertex(data, pPrim->V2);
- var v3 = LocalVertex(data, pPrim->V3);
- var w = obj->World.M;
- var w1 = SharpDX.Vector3.TransformCoordinate(new(v1.X, v1.Y, v1.Z), w).ToSystem();
- var w2 = SharpDX.Vector3.TransformCoordinate(new(v2.X, v2.Y, v2.Z), w).ToSystem();
- var w3 = SharpDX.Vector3.TransformCoordinate(new(v3.X, v3.Y, v3.Z), w).ToSystem();
- Camera.Instance?.DrawWorldLine(w1, w2, color);
- Camera.Instance?.DrawWorldLine(w2, w3, color);
- Camera.Instance?.DrawWorldLine(w3, w1, color);
- }
-
- private void VisualizeShape(CollisionShapePCBData* data, CollisionObjectShape* obj, uint color = ArenaColor.Danger)
- {
- for (int i = 0; i < data->NumPrims; ++i)
- VisualizePrimitive(data, i, obj, color);
- if (data->Child1Offset != 0)
- VisualizeShape((CollisionShapePCBData*)((byte*)data + data->Child1Offset), obj, color);
- if (data->Child2Offset != 0)
- VisualizeShape((CollisionShapePCBData*)((byte*)data + data->Child2Offset), obj, color);
- }
-
- private void VisualizeObject(CollisionObjectBase* obj)
- {
- switch (obj->Vtbl->GetObjectType(obj))
- {
- case CollisionObjectType.Multi:
- {
- var castObj = (CollisionObjectMulti*)obj;
- if (castObj->Elements != null)
- {
- for (int i = 0; i < *castObj->PtrNumElements; ++i)
- {
- var elem = castObj->Elements + i;
- if (elem->Shape != null && elem->Shape->Shape != null && (nint)elem->Shape->Shape->Vtbl == _typeinfoCollisionShapePCB && elem->Shape->Shape->Data != null)
- VisualizeShape(elem->Shape->Shape->Data, elem->Shape, ArenaColor.Safe);
- }
- }
- }
- break;
- case CollisionObjectType.Shape:
- {
- var castObj = (CollisionObjectShape*)obj;
- if (castObj->Shape != null && (nint)castObj->Shape->Vtbl == _typeinfoCollisionShapePCB && castObj->Shape->Data != null)
- VisualizeShape(castObj->Shape->Data, castObj, _slaveShapes.Contains((nint)obj) ? ArenaColor.Safe : ArenaColor.Danger);
- }
- break;
- case CollisionObjectType.Box:
- {
- var castObj = (CollisionObjectBox*)obj;
- Camera.Instance?.DrawWorldOBB(new(-1), new(+1), castObj->World.M, ArenaColor.Enemy);
- }
- break;
- case CollisionObjectType.Cylinder:
- {
- var castObj = (CollisionObjectCylinder*)obj;
- Camera.Instance?.DrawWorldUnitCylinder(castObj->World.M, ArenaColor.Enemy);
- }
- break;
- case CollisionObjectType.Sphere:
- {
- var castObj = (CollisionObjectSphere*)obj;
- Camera.Instance?.DrawWorldSphere(castObj->Translation, castObj->Scale.X, ArenaColor.Enemy);
- }
- break;
- case CollisionObjectType.Plane:
- case CollisionObjectType.PlaneTwoSided:
- {
- var castObj = (CollisionObjectPlane*)obj;
- var m = castObj->World.M;
- var a = SharpDX.Vector3.TransformCoordinate(new(-1, +1, 0), m).ToSystem();
- var b = SharpDX.Vector3.TransformCoordinate(new(-1, -1, 0), m).ToSystem();
- var c = SharpDX.Vector3.TransformCoordinate(new(+1, -1, 0), m).ToSystem();
- var d = SharpDX.Vector3.TransformCoordinate(new(+1, +1, 0), m).ToSystem();
- Camera.Instance?.DrawWorldLine(a, b, ArenaColor.Enemy);
- Camera.Instance?.DrawWorldLine(b, c, ArenaColor.Enemy);
- Camera.Instance?.DrawWorldLine(c, d, ArenaColor.Enemy);
- Camera.Instance?.DrawWorldLine(d, a, ArenaColor.Enemy);
- }
- break;
- }
- }
-
- private void VisualizeQuadtreeNode(CollisionNode* node)
- {
- var child = (CollisionObjectBase*)node->Next;
- while (child != null && child != node)
- {
- VisualizeObject(child);
- child = child->NextNodeObj;
- }
- }
-
- private void VisualizeScene(CollisionScene* scene)
- {
- var obj = scene->FirstObj;
- while (obj != null)
- {
- VisualizeObject(obj);
- obj = (CollisionObjectBase*)obj->Base.Next;
- }
- }
-
- private Vector3 LocalVertex(CollisionShapePCBData* data, int index)
- {
- var pRaw = (float*)(data + 1);
- if (index < data->NumVertsRaw)
- {
- pRaw += 3 * index;
- return new(pRaw[0], pRaw[1], pRaw[2]);
- }
- var pCompr = (ushort*)(pRaw + 3 * data->NumVertsRaw);
- pCompr += 3 * (index - data->NumVertsRaw);
- var quantScale = (data->AABBMax - data->AABBMin) / 65535.0f;
- return data->AABBMin + quantScale * new Vector3(pCompr[0], pCompr[1], pCompr[2]);
- }
-
- private void ExportToObj(bool streamed, bool nonStreamedShapes)
- {
- var res = new StringBuilder();
- var firstVertex = 1;
-
- var scene = CollisionModule.Instance->Manager->FirstScene;
- var identity = SharpDX.Matrix.Identity;
- while (scene != null)
- {
- var obj = scene->Scene->FirstObj;
- while (obj != null)
- {
- switch (obj->Vtbl->GetObjectType(obj))
- {
- case CollisionObjectType.Multi:
- if (streamed)
- {
- var castObj = (CollisionObjectMulti*)obj;
- if (castObj->Elements != null)
- {
- var basePath = MemoryHelper.ReadStringNullTerminated((nint)castObj->PathBase);
- for (int i = 0; i < *castObj->PtrNumElements; ++i)
- {
- var f = Service.DataManager.GetFile($"{basePath}/tr{castObj->Elements[i].SubFile:d4}.pcb");
- if (f != null)
- {
- // format: dword 0, dword version (1/4), dword totalChildNodes, dword totalPrims, pcbdata
- fixed (byte* data = &f.Data[0])
- {
- var version = *(int*)(data + 4);
- if (version is 1 or 4)
- {
- ExportShape(res, ref identity, (CollisionShapePCBData*)(data + 16), ref firstVertex);
- }
- }
- }
- }
- }
- }
- break;
- case CollisionObjectType.Shape:
- if (nonStreamedShapes && !_slaveShapes.Contains((nint)obj))
- {
- var castObj = (CollisionObjectShape*)obj;
- if (castObj->Shape != null && (nint)castObj->Shape->Vtbl == _typeinfoCollisionShapePCB && castObj->Shape->Data != null)
- {
- var m = castObj->World.M;
- ExportShape(res, ref m, castObj->Shape->Data, ref firstVertex);
- }
- }
- break;
- }
- obj = (CollisionObjectBase*)obj->Base.Next;
- }
- scene = (CollisionSceneWrapper*)scene->Base.Next;
- }
- ImGui.SetClipboardText(res.ToString());
- }
-
- private void ExportShape(StringBuilder res, ref SharpDX.Matrix world, CollisionShapePCBData* data, ref int firstVertex)
- {
- var pRaw = (float*)(data + 1);
- for (int i = 0; i < data->NumVertsRaw; ++i)
- {
- var v = new Vector3(pRaw[0], pRaw[1], pRaw[2]);
- var w = SharpDX.Vector3.TransformCoordinate(new(v.X, v.Y, v.Z), world);
- res.AppendLine($"v {w.X} {w.Y} {w.Z}");
- pRaw += 3;
- }
- var pCompressed = (ushort*)pRaw;
- var quantScale = (data->AABBMax - data->AABBMin) / 65535.0f;
- for (int i = 0; i < data->NumVertsCompressed; ++i)
- {
- var v = data->AABBMin + quantScale * new Vector3(pCompressed[0], pCompressed[1], pCompressed[2]);
- var w = SharpDX.Vector3.TransformCoordinate(new(v.X, v.Y, v.Z), world);
- res.AppendLine($"v {w.X} {w.Y} {w.Z}");
- pCompressed += 3;
- }
- var pPrims = (CollisionShapePrimitive*)pCompressed;
- for (int i = 0; i < data->NumPrims; ++i)
- {
- res.AppendLine($"f {pPrims->V1 + firstVertex} {pPrims->V2 + firstVertex} {pPrims->V3 + firstVertex}");
- ++pPrims;
- }
- firstVertex += data->NumVertsRaw + data->NumVertsCompressed;
-
- if (data->Child1Offset != 0)
- ExportShape(res, ref world, (CollisionShapePCBData*)((byte*)data + data->Child1Offset), ref firstVertex);
- if (data->Child2Offset != 0)
- ExportShape(res, ref world, (CollisionShapePCBData*)((byte*)data + data->Child2Offset), ref firstVertex);
- }
-
- private void Report()
- {
- Dictionary shapeVtbls = [];
- Dictionary multiVersions = [];
-
- var scene = CollisionModule.Instance->Manager->FirstScene;
- while (scene != null)
- {
- var obj = scene->Scene->FirstObj;
- while (obj != null)
- {
- switch (obj->Vtbl->GetObjectType(obj))
- {
- case CollisionObjectType.Multi:
- {
- var castObj = (CollisionObjectMulti*)obj;
- if (castObj->Elements != null)
- {
- var basePath = MemoryHelper.ReadStringNullTerminated((nint)castObj->PathBase);
- for (int i = 0; i < *castObj->PtrNumElements; ++i)
- {
- var f = Service.DataManager.GetFile($"{basePath}/tr{castObj->Elements[i].SubFile:d4}.pcb");
- if (f != null)
- {
- // format: dword 0, dword version (1/4), dword totalChildNodes, dword totalPrims, pcbdata
- fixed (byte* data = &f.Data[0])
- {
- var version = *(int*)(data + 4);
- if (!multiVersions.ContainsKey(version))
- multiVersions[version] = 0;
- ++multiVersions[version];
- }
- }
- }
- }
- }
- break;
- case CollisionObjectType.Shape:
- {
- var castObj = (CollisionObjectShape*)obj;
- if (castObj->Shape != null)
- {
- var vt = (nint)castObj->Shape->Vtbl;
- if (!shapeVtbls.ContainsKey(vt))
- shapeVtbls[vt] = 0;
- ++shapeVtbls[vt];
- }
- }
- break;
- }
- obj = (CollisionObjectBase*)obj->Base.Next;
- }
- scene = (CollisionSceneWrapper*)scene->Base.Next;
- }
-
- var res = new StringBuilder();
- res.AppendLine("multi versions:");
- foreach (var v in multiVersions)
- res.AppendLine($"v{v.Key} == {v.Value}");
- res.AppendLine("shape vtbls:");
- foreach (var vt in shapeVtbls)
- res.AppendLine($"{vt.Key - Service.SigScanner.Module.BaseAddress:X} == {vt.Value}");
- ImGui.SetClipboardText(res.ToString());
- }
-
- private string SphereStr(Vector4 s) => $"[{s.X:f3}, {s.Y:f3}, {s.Z:f3}] R{s.W:f3}";
-}
diff --git a/BossMod/Debug/DebugInput.cs b/BossMod/Debug/DebugInput.cs
index bdb02d8e0c..49efefd0a6 100644
--- a/BossMod/Debug/DebugInput.cs
+++ b/BossMod/Debug/DebugInput.cs
@@ -144,7 +144,7 @@ public void Dispose()
public void Draw()
{
- var dt = Utils.FrameDuration();
+ var dt = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->FrameDeltaTime;
var player = _ws.Party.Player();
var curPos = player?.PosRot.XYZ() ?? new();
@@ -312,7 +312,7 @@ private void DrawGamepad()
}
}
- private InputData* GetInputData() => (InputData*)FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->GetUiModule()->GetUIInputData();
+ private InputData* GetInputData() => (InputData*)FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->GetUIModule()->GetUIInputData();
private void RMIWalkDetour(PlayerMoveControllerWalk* self, float* sumLeft, float* sumForward, float* sumTurnLeft, byte* haveBackwardOrStrafe, byte* a6, byte bAdditiveUnk)
{
@@ -354,10 +354,11 @@ private void RMICameraDetour(CameraX* self, int inputMode, float speedH, float s
_rmiCameraHook.Original(self, inputMode, speedH, speedV);
if (inputMode == 0) // let user override...
{
+ var dt = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->FrameDeltaTime;
var deltaH = (_pmcDesiredAzimuth.Degrees() - self->DirH.Radians()).Normalized();
var deltaV = (_pmcDesiredAltitude.Degrees() - self->DirV.Radians()).Normalized();
- var maxH = _pmcCameraSpeedH.Degrees().Rad * Utils.FrameDuration();
- var maxV = _pmcCameraSpeedV.Degrees().Rad * Utils.FrameDuration();
+ var maxH = _pmcCameraSpeedH.Degrees().Rad * dt;
+ var maxV = _pmcCameraSpeedV.Degrees().Rad * dt;
self->InputDeltaH = Math.Clamp(deltaH.Rad, -maxH, maxH);
self->InputDeltaV = Math.Clamp(deltaV.Rad, -maxV, maxV);
}
diff --git a/BossMod/Debug/DebugObjects.cs b/BossMod/Debug/DebugObjects.cs
index 1e6a0b8351..13d9be2466 100644
--- a/BossMod/Debug/DebugObjects.cs
+++ b/BossMod/Debug/DebugObjects.cs
@@ -1,5 +1,4 @@
using Dalamud.Game.ClientState.Objects.Types;
-using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using ImGuiNET;
using System.Text;
@@ -10,7 +9,7 @@ public class DebugObjects
{
private readonly UITree _tree = new();
private bool _showCrap;
- private uint _selectedID;
+ private ulong _selectedID;
public unsafe void DrawObjectTable()
{
@@ -26,28 +25,29 @@ public unsafe void DrawObjectTable()
continue;
var internalObj = Utils.GameObjectInternal(obj);
- var localID = Utils.ReadField(internalObj, 0x78);
- var uniqueID = obj.ObjectId != 0xE0000000 ? obj.ObjectId : localID;
+ var localID = internalObj->LayoutId;
+ ulong uniqueID = internalObj->GetObjectId();
var posRot = new Vector4(obj.Position.X, obj.Position.Y, obj.Position.Z, obj.Rotation);
foreach (var n in _tree.Node($"#{i} {Utils.ObjectString(obj)} ({localID:X}) ({Utils.ObjectKindString(obj)}) {Utils.PosRotString(posRot)}###{uniqueID:X}", contextMenu: () => ObjectContextMenu(obj), select: () => _selectedID = uniqueID))
{
var character = obj as Character;
var battleChara = obj as BattleChara;
- var internalChara = Utils.BattleCharaInternal(battleChara);
+ var internalChara = Utils.CharacterInternal(character);
+ _tree.LeafNode($"Unique ID: {uniqueID:X}");
_tree.LeafNode($"Gimmick ID: {Utils.ReadField(internalObj, 0x7C):X}");
_tree.LeafNode($"Radius: {obj.HitboxRadius:f3}");
_tree.LeafNode($"Owner: {Utils.ObjectString(obj.OwnerId)}");
- _tree.LeafNode($"BNpcBase/Name: {obj.DataId}/{Utils.GameObjectInternal(obj)->GetNpcID()}");
+ _tree.LeafNode($"BNpcBase/Name: {obj.DataId:X}/{Utils.GameObjectInternal(obj)->GetNameId()}");
_tree.LeafNode($"Targetable: {obj.IsTargetable}");
- _tree.LeafNode($"Friendly: {Utils.GameObjectIsFriendly(obj)}");
+ _tree.LeafNode($"Friendly: {Utils.GameObjectIsFriendly(Utils.GameObjectInternal(obj))}");
_tree.LeafNode($"Is character: {internalObj->IsCharacter()}");
- _tree.LeafNode($"Event state: {Utils.GameObjectEventState(obj)}");
+ _tree.LeafNode($"Event state: {Utils.GameObjectInternal(obj)->EventState}");
if (character != null)
{
_tree.LeafNode($"Class: {(Class)character.ClassJob.Id} ({character.ClassJob.Id})");
- _tree.LeafNode($"HP: {character.CurrentHp}/{character.MaxHp} ({Utils.CharacterShieldValue(character)})");
+ _tree.LeafNode($"HP: {character.CurrentHp}/{character.MaxHp} ({internalChara->ShieldValue})");
_tree.LeafNode($"Status flags: {character.StatusFlags}");
}
if (battleChara != null)
@@ -89,7 +89,6 @@ public unsafe void DrawObjectTable()
public unsafe void DrawUIObjects()
{
var module = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->UIModule->GetUI3DModule();
- var objs = (FFXIVClientStructs.FFXIV.Client.UI.UI3DModule.ObjectInfo*)module->ObjectInfoArray;
ImGui.BeginTable("uiobj", 3, ImGuiTableFlags.Resizable);
ImGui.TableSetupColumn("Index");
ImGui.TableSetupColumn("GameObj");
@@ -97,11 +96,11 @@ public unsafe void DrawUIObjects()
ImGui.TableHeadersRow();
for (int i = 0; i < 426; ++i)
{
- var o = objs[i].GameObject;
+ var o = module->ObjectInfos[i].GameObject;
ImGui.TableNextRow();
ImGui.TableNextColumn(); ImGui.TextUnformatted($"{i}: {(ulong)o:X}");
- ImGui.TableNextColumn(); if (o != null) ImGui.TextUnformatted($"{o->DataID:X} '{MemoryHelper.ReadSeString((IntPtr)o->Name, 64)}' <{o->ObjectID:X}>");
- ImGui.TableNextColumn(); ImGui.TextUnformatted($"{objs[i].NamePlateObjectKind}");
+ ImGui.TableNextColumn(); if (o != null) ImGui.TextUnformatted($"{o->BaseId:X} '{o->NameString}' <{o->EntityId:X}>");
+ ImGui.TableNextColumn(); ImGui.TextUnformatted($"{module->ObjectInfos[i].NamePlateObjectKind}");
}
ImGui.EndTable();
}
@@ -119,14 +118,14 @@ public static unsafe void DumpObjectTable()
}
var chara = obj as BattleChara;
- if (chara)
+ if (chara != null)
{
res.Append($", vfxObj=0x{Utils.ReadField(internalObj, 0x1840):X}/0x{Utils.ReadField(internalObj, 0x1848):X}");
- if (chara!.IsCasting)
+ if (chara.IsCasting)
{
var target = Service.ObjectTable.SearchById(chara.CastTargetObjectId);
var targetString = target ? Utils.ObjectString(target!) : "unknown";
- res.Append($", castAction={new ActionID((ActionType)chara.CastActionType, chara.CastActionId)}, castTarget={targetString}, castLoc={Utils.Vec3String(Utils.BattleCharaCastLocation(chara))}, castTime={Utils.CastTimeString(chara.CurrentCastTime, chara.TotalCastTime)}");
+ res.Append($", castAction={new ActionID((ActionType)chara.CastActionType, chara.CastActionId)}, castTarget={targetString}, castLoc={Utils.Vec3String(Utils.BattleCharaInternal(chara)->GetCastInfo()->CastLocation)}, castTime={Utils.CastTimeString(chara.CurrentCastTime, chara.TotalCastTime)}");
}
foreach (var status in chara!.StatusList)
{
diff --git a/BossMod/Debug/DebugParty.cs b/BossMod/Debug/DebugParty.cs
index 4363180d7a..bd65c8beec 100644
--- a/BossMod/Debug/DebugParty.cs
+++ b/BossMod/Debug/DebugParty.cs
@@ -1,13 +1,12 @@
-using Dalamud.Memory;
-using FFXIVClientStructs.FFXIV.Client.Game.Group;
+using FFXIVClientStructs.FFXIV.Client.Game.Group;
+using FFXIVClientStructs.FFXIV.Client.Game.Object;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
using ImGuiNET;
namespace BossMod;
class DebugParty
{
- readonly PartyAlliance _alliance = new();
-
public void DrawPartyDalamud()
{
// note: alliance doesn't seem to work correctly, IsAlliance is always false and AllianceMembers are not filled...
@@ -39,7 +38,9 @@ public unsafe void DrawPartyCustom()
{
// note: alliance slots, unlike normal slots, are more permanent - if a player leaves, other players retain their indices (leaving gaps)
// also content ID for all alliance members always seems to be 0; this isn't a huge deal, since alliance members are always in the same zone and thus have valid object IDs
- ImGui.TextUnformatted($"Num members: {_alliance.NumPartyMembers}, alliance={(_alliance.IsAlliance ? (_alliance.IsSmallGroupAlliance ? "small-group" : "yes") : "no")}");
+ var gm = GroupManager.Instance();
+ var ui = UIState.Instance();
+ ImGui.TextUnformatted($"Num members: {gm->MemberCount}, alliance={(!gm->IsAlliance ? "no" : gm->IsSmallGroupAlliance ? "small-group" : "yes")}, has-helpers={ui->Buddy.DutyHelperInfo.HasHelpers}");
ImGui.BeginTable("party-custom", 7, ImGuiTableFlags.Resizable);
ImGui.TableSetupColumn("Index");
@@ -50,24 +51,45 @@ public unsafe void DrawPartyCustom()
ImGui.TableSetupColumn("World");
ImGui.TableSetupColumn("Position");
ImGui.TableHeadersRow();
- for (int i = 0; i < _alliance.NumPartyMembers; ++i)
- DrawPartyMember($"P{i}", _alliance.PartyMember(i));
- for (int i = 0; i < PartyAlliance.MaxAllianceMembers; ++i)
- DrawPartyMember($"A{i}", _alliance.AllianceMember(i));
+ for (int i = 0; i < gm->MemberCount; ++i)
+ DrawPartyMember($"P{i}", ref gm->PartyMembers[i]);
+ for (int i = 0; i < gm->AllianceMembers.Length; ++i)
+ if (gm->AllianceMembers[i].IsValidAllianceMember)
+ DrawPartyMember($"A{i}", ref gm->AllianceMembers[i]);
+ for (int i = 0; i < ui->Buddy.DutyHelperInfo.ENpcIds.Length; ++i)
+ {
+ var id = ui->Buddy.DutyHelperInfo.DutyHelpers[i].ObjectId;
+ if (id == 0xE0000000)
+ continue;
+ var obj = GameObjectManager.Instance()->Objects.GetNetworkedObjectById(id);
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted($"B{i}");
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted($"{ui->Buddy.DutyHelperInfo.ENpcIds[i]}");
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted($"{id:X}");
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted($"{obj->NameString}");
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted("---");
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted("---");
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted($"{Utils.Vec3String(obj->Position)}");
+ }
ImGui.EndTable();
}
- private unsafe void DrawPartyMember(string index, PartyMember* member)
+ private unsafe void DrawPartyMember(string index, ref PartyMember member)
{
- if (member == null)
- return;
ImGui.TableNextRow();
ImGui.TableNextColumn(); ImGui.TextUnformatted(index);
- ImGui.TableNextColumn(); ImGui.TextUnformatted($"{member->ContentID:X}");
- ImGui.TableNextColumn(); ImGui.TextUnformatted($"{member->ObjectID:X}");
- ImGui.TableNextColumn(); ImGui.TextUnformatted(MemoryHelper.ReadSeString((IntPtr)member->Name, 0x40).ToString());
- ImGui.TableNextColumn(); ImGui.TextUnformatted($"{member->TerritoryType}");
- ImGui.TableNextColumn(); ImGui.TextUnformatted($"{member->HomeWorld}");
- ImGui.TableNextColumn(); ImGui.TextUnformatted(Utils.Vec3String(new(member->X, member->Y, member->Z)));
+ ImGui.TableNextColumn(); ImGui.TextUnformatted($"{member.ContentId:X}");
+ ImGui.TableNextColumn(); ImGui.TextUnformatted($"{member.ObjectId:X}");
+ ImGui.TableNextColumn(); ImGui.TextUnformatted(member.NameString);
+ ImGui.TableNextColumn(); ImGui.TextUnformatted($"{member.TerritoryType}");
+ ImGui.TableNextColumn(); ImGui.TextUnformatted($"{member.HomeWorld}");
+ ImGui.TableNextColumn(); ImGui.TextUnformatted(Utils.Vec3String(new(member.X, member.Y, member.Z)));
}
}
diff --git a/BossMod/Debug/DebugTiming.cs b/BossMod/Debug/DebugTiming.cs
index 47d23b0f90..cdb51bf7e9 100644
--- a/BossMod/Debug/DebugTiming.cs
+++ b/BossMod/Debug/DebugTiming.cs
@@ -5,30 +5,27 @@ namespace BossMod;
public class DebugTiming
{
uint _prevFrameCounter;
- ulong _prevQPC;
+ long _prevQPC;
public unsafe void Draw()
{
var fwk = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
- var qpf = Utils.FrameQPF();
- var qpc = Utils.FrameQPC();
- var dtRaw = Utils.FrameDurationRaw();
- var dtReal = (double)(qpc - _prevQPC) / qpf;
+ var dtReal = (double)(fwk->PerformanceCounterValue - _prevQPC) / fwk->PerformanceCounterFrequency;
ImGui.TextUnformatted($"Frame counter: {fwk->FrameCounter}");
ImGui.TextUnformatted($"Frame time effective: {fwk->FrameDeltaTime}");
ImGui.TextUnformatted($"Framerate: {fwk->FrameRate}");
- ImGui.TextUnformatted($"Forced frame duration: {Utils.ReadField(fwk, 0x16C0)}");
- ImGui.TextUnformatted($"Forced next frame duration: {Utils.ReadField(fwk, 0x17CC)}");
- ImGui.TextUnformatted($"Frame duration multiplier: {Utils.ReadField(fwk, 0x16C4)}");
- ImGui.TextUnformatted($"Tick speed multiplier: {Utils.TickSpeedMultiplier()}");
- ImGui.TextUnformatted($"QPC freq: {qpf}");
- ImGui.TextUnformatted($"QPC value: {qpc}");
- ImGui.TextUnformatted($"dt raw: {dtRaw}");
- ImGui.TextUnformatted($"dt real: {dtReal} = raw + {dtReal - dtRaw}");
- ImGui.TextUnformatted($"dt ms granularity: {Utils.ReadField(fwk, 0x16D0)} + {Utils.ReadField(fwk, 0x16D8)}");
- ImGui.TextUnformatted($"dt us granularity: {Utils.ReadField(fwk, 0x16E0)} + {Utils.ReadField(fwk, 0x16E8)}");
+ ImGui.TextUnformatted($"Forced frame duration: {fwk->FrameDeltaTimeOverride}");
+ ImGui.TextUnformatted($"Forced next frame duration: {fwk->NextFrameDeltaTimeOverride}");
+ ImGui.TextUnformatted($"Frame duration multiplier: {fwk->FrameDeltaFactor}");
+ ImGui.TextUnformatted($"Tick speed multiplier: {fwk->GameSpeedMultiplier}");
+ ImGui.TextUnformatted($"QPC freq: {fwk->PerformanceCounterFrequency}");
+ ImGui.TextUnformatted($"QPC value: {fwk->PerformanceCounterValue}");
+ ImGui.TextUnformatted($"dt raw: {fwk->RealFrameDeltaTime}");
+ ImGui.TextUnformatted($"dt real: {dtReal} = raw + {dtReal - fwk->RealFrameDeltaTime}");
+ ImGui.TextUnformatted($"dt ms granularity: {fwk->FrameDeltaTimeMSInt} + {fwk->FrameDeltaTimeMSRem}");
+ ImGui.TextUnformatted($"dt us granularity: {fwk->FrameDeltaTimeUSInt} + {fwk->FrameDeltaTimeUSRem}");
ImGui.TextUnformatted($"dt timer: {DateTime.UnixEpoch.AddSeconds(fwk->UtcTime.TimeStamp)}");
_prevFrameCounter = fwk->FrameCounter;
- _prevQPC = qpc;
+ _prevQPC = fwk->PerformanceCounterValue;
}
}
diff --git a/BossMod/Debug/MainDebugWindow.cs b/BossMod/Debug/MainDebugWindow.cs
index 259547aca1..372d8cbf26 100644
--- a/BossMod/Debug/MainDebugWindow.cs
+++ b/BossMod/Debug/MainDebugWindow.cs
@@ -1,5 +1,6 @@
using Dalamud.Game.ClientState.Objects.Types;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using ImGuiNET;
namespace BossMod;
@@ -16,7 +17,6 @@ namespace BossMod;
private readonly DebugClassDefinitions _debugClassDefinitions = new(ws);
private readonly DebugAddon _debugAddon = new();
private readonly DebugTiming _debugTiming = new();
- private readonly DebugCollision _debugCollision = new();
private readonly DebugVfx _debugVfx = new();
protected override void Dispose(bool disposing)
@@ -24,15 +24,15 @@ protected override void Dispose(bool disposing)
_debugInput.Dispose();
_debugClassDefinitions.Dispose();
_debugAddon.Dispose();
- _debugCollision.Dispose();
_debugVfx.Dispose();
base.Dispose(disposing);
}
public unsafe override void Draw()
{
+ var playerCID = UIState.Instance()->PlayerState.ContentId;
var player = Service.ClientState.LocalPlayer;
- ImGui.TextUnformatted($"Current zone: {ws.CurrentZone}, player=0x{(ulong)Utils.GameObjectInternal(player):X}, playerCID={Service.ClientState.LocalContentId:X}, pos = {Utils.Vec3String(player?.Position ?? new Vector3())}");
+ ImGui.TextUnformatted($"Current zone: {ws.CurrentZone}, player=0x{(ulong)Utils.GameObjectInternal(player):X}, playerCID={playerCID:X}, pos = {Utils.Vec3String(player?.Position ?? new Vector3())}");
ImGui.TextUnformatted($"ID scramble: {Network.IDScramble.Delta} = {*Network.IDScramble.OffsetAdjusted} - {*Network.IDScramble.OffsetBaseFixed} - {*Network.IDScramble.OffsetBaseChanging}");
ImGui.TextUnformatted($"Player mode: {Utils.CharacterInternal(player)->Mode}");
@@ -134,10 +134,6 @@ public unsafe override void Draw()
{
DrawWindowSystem();
}
- if (ImGui.CollapsingHeader("Collision"))
- {
- _debugCollision.Draw();
- }
if (ImGui.CollapsingHeader("VFX"))
{
_debugVfx.Draw();
@@ -237,7 +233,7 @@ private unsafe void DrawTarget(string kind, FFXIVClientStructs.FFXIV.Client.Game
var dist = selfToObj.Length();
var angle = Angle.FromDirection(new(selfToObj.XZ())) - refAngle;
var visHalf = Angle.Asin(obj->HitboxRadius / dist);
- ImGui.TextUnformatted($"{kind}: #{obj->ObjectIndex} {Utils.ObjectString(obj->ObjectID)} {obj->DataID}:{obj->GetNpcID()}, hb={obj->HitboxRadius} ({visHalf}), dist={dist}, angle={angle} ({Math.Max(0, angle.Abs().Rad - visHalf.Rad).Radians()})");
+ ImGui.TextUnformatted($"{kind}: #{obj->ObjectIndex} {Utils.ObjectString(obj->EntityId)} {obj->BaseId}:{obj->GetNameId()}, hb={obj->HitboxRadius} ({visHalf}), dist={dist}, angle={angle} ({Math.Max(0, angle.Abs().Rad - visHalf.Rad).Radians()})");
}
private unsafe void DrawPlayerAttributes()
@@ -247,7 +243,7 @@ private unsafe void DrawPlayerAttributes()
Utils.WriteField((void*)Service.Condition.Address, (int)Dalamud.Game.ClientState.Conditions.ConditionFlag.OnFreeTrial, false);
}
- var uiState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState.Instance();
+ var uiState = UIState.Instance();
ImGui.BeginTable("attrs", 2);
ImGui.TableSetupColumn("Index");
ImGui.TableSetupColumn("Value");
@@ -265,10 +261,10 @@ private unsafe void DrawPlayerAttributes()
private unsafe void DrawCountdown()
{
- var agent = Countdown.Instance;
- ImGui.TextUnformatted($"Active: {agent->Active != 0}");
- ImGui.TextUnformatted($"Initiator: {Utils.ObjectString(agent->Initiator)}");
- ImGui.TextUnformatted($"Time left: {agent->Timer:f3}");
+ var agent = AgentCountDownSettingDialog.Instance();
+ ImGui.TextUnformatted($"Active: {agent->Active} (showing cd={agent->ShowingCountdown})");
+ ImGui.TextUnformatted($"Initiator: {Utils.ObjectString(agent->InitiatorId)}");
+ ImGui.TextUnformatted($"Time left: {agent->TimeRemaining:f3}");
}
private void DrawWindowSystem()
@@ -283,7 +279,7 @@ private void DrawWindowSystem()
private unsafe void DrawLimitBreak()
{
var lb = LimitBreakController.Instance();
- ImGui.TextUnformatted($"Value: {lb->CurrentValue}/{lb->BarValue & 0xFFFF} ({lb->BarCount} bars)");
- ImGui.TextUnformatted($"Unks: uE={(lb->BarValue >> 16) & 0xFF}, uF={lb->BarValue >> 24}");
+ ImGui.TextUnformatted($"Value: {lb->CurrentUnits}/{lb->BarUnits} ({lb->BarCount} bars)");
+ ImGui.TextUnformatted($"PVP: {lb->IsPvP}");
}
}
diff --git a/BossMod/Framework/ActionManagerEx.cs b/BossMod/Framework/ActionManagerEx.cs
index 150a62f8f8..58b4f98fbb 100644
--- a/BossMod/Framework/ActionManagerEx.cs
+++ b/BossMod/Framework/ActionManagerEx.cs
@@ -1,8 +1,8 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
-using System.Runtime.InteropServices;
namespace BossMod;
@@ -46,52 +46,22 @@ unsafe sealed class ActionManagerEx : IDisposable
{
public static ActionManagerEx? Instance;
+ public ActionID CastSpell => new(ActionType.Spell, _inst->CastSpellId);
+ public ActionID CastAction => new((ActionType)_inst->CastActionType, _inst->CastActionId);
+ public float CastTimeRemaining => _inst->CastSpellId != 0 ? _inst->CastTimeTotal - _inst->CastTimeElapsed : 0;
+ public float ComboTimeLeft => _inst->Combo.Timer;
+ public uint ComboLastMove => _inst->Combo.Action;
+ public ActionID QueuedAction => new((ActionType)_inst->QueuedActionType, _inst->QueuedActionId);
+
public float AnimationLockDelaySmoothing = 0.8f; // TODO tweak
public float AnimationLockDelayAverage { get; private set; } = 0.1f; // smoothed delay between client request and server response
public float AnimationLockDelayMax => Config.RemoveAnimationLockDelay ? 0 : float.MaxValue; // this caps max delay a-la xivalexander (TODO: make tweakable?)
- public float AnimationLock => Utils.ReadField(_inst, 8);
-
- public uint CastSpellID => Utils.ReadField(_inst, 0x24);
- public ActionID CastSpell => new(ActionType.Spell, CastActionID);
- public ActionType CastActionType => (ActionType)Utils.ReadField(_inst, 0x28);
- public uint CastActionID => Utils.ReadField(_inst, 0x2C);
- public ActionID CastAction => new(CastActionType, CastActionID);
- public float CastTimeElapsed => Utils.ReadField(_inst, 0x30);
- public float CastTimeTotal => Utils.ReadField(_inst, 0x34);
- public float CastTimeRemaining => CastSpellID != 0 ? CastTimeTotal - CastTimeElapsed : 0;
- public ulong CastTargetID => Utils.ReadField(_inst, 0x38);
- public Vector3 CastTargetPos => Utils.ReadField(_inst, 0x40);
-
- public float ComboTimeLeft => Utils.ReadField(_inst, 0x60);
- public uint ComboLastMove => Utils.ReadField(_inst, 0x64);
-
- public bool QueueActive => Utils.ReadField(_inst, 0x68);
- public ActionType QueueActionType => (ActionType)Utils.ReadField(_inst, 0x6C);
- public uint QueueActionID => Utils.ReadField(_inst, 0x70);
- public ActionID QueueAction => new(QueueActionType, QueueActionID);
- public ulong QueueTargetID => Utils.ReadField(_inst, 0x78);
- public uint QueueCallType => Utils.ReadField(_inst, 0x80);
- public uint QueueComboRouteID => Utils.ReadField(_inst, 0x84);
-
- public uint GTActionID => Utils.ReadField(_inst, 0x88);
- public ActionType GTActionType => (ActionType)Utils.ReadField(_inst, 0x8C);
- public ActionID GTAction => new(GTActionType, GTActionID);
- public uint GTSpellID => Utils.ReadField(_inst, 0x90);
- public ActionID GTSpell => new(ActionType.Spell, GTSpellID);
- public uint GTUnkArg => Utils.ReadField(_inst, 0x94);
- public ulong GTUnkObj => Utils.ReadField(_inst, 0x98);
- public byte GTUnkA0 => Utils.ReadField(_inst, 0xA0);
- public byte GTUnkB8 => Utils.ReadField(_inst, 0xB8);
- public uint GTUnkBC => Utils.ReadField(_inst, 0xBC);
-
- public ushort LastUsedActionSequence => Utils.ReadField(_inst, 0x110);
-
- public float EffectiveAnimationLock => AnimationLock + CastTimeRemaining; // animation lock starts ticking down only when cast ends
+
+ public float EffectiveAnimationLock => _inst->AnimationLock + CastTimeRemaining; // animation lock starts ticking down only when cast ends
public float EffectiveAnimationLockDelay => AnimationLockDelayMax <= 0.5f ? AnimationLockDelayMax : MathF.Min(AnimationLockDelayAverage, 0.1f); // this is a conservative estimate
public Event ActionRequested = new();
public Event ActionEffectReceived = new();
- public Event EffectResultReceived = new();
public InputOverride InputOverride;
public ActionManagerConfig Config;
@@ -104,32 +74,13 @@ unsafe sealed class ActionManagerEx : IDisposable
private (Angle pre, Angle post)? _restoreRotation; // if not null, we'll try restoring rotation to pre while it is equal to post
private int _restoreCntr;
- private delegate bool GetGroundTargetPositionDelegate(ActionManager* self, Vector3* outPos);
- private readonly GetGroundTargetPositionDelegate _getGroundTargetPositionFunc;
-
- private delegate void FaceTargetDelegate(ActionManager* self, Vector3* position, ulong targetID);
- private readonly FaceTargetDelegate _faceTargetFunc;
-
- private delegate void UpdateDelegate(ActionManager* self);
- private readonly Hook _updateHook;
-
- private delegate bool UseActionLocationDelegate(ActionManager* self, ActionType actionType, uint actionID, ulong targetID, Vector3* targetPos, uint itemLocation);
- private readonly Hook _useActionLocationHook;
-
- private delegate bool UseBozjaFromHolsterDirectorDelegate(void* self, uint holsterIndex, uint slot);
- private readonly Hook _useBozjaFromHolsterDirectorHook;
+ private readonly HookAddress _updateHook;
+ private readonly HookAddress _useActionLocationHook;
+ private readonly HookAddress _useBozjaFromHolsterDirectorHook;
private delegate void ProcessPacketActionEffectDelegate(uint casterID, FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara* casterObj, Vector3* targetPos, Network.ServerIPC.ActionEffectHeader* header, ulong* effects, ulong* targets);
private readonly Hook _processPacketActionEffectHook;
- private delegate void ProcessPacketEffectResultDelegate(uint targetID, byte* packet, byte replaying);
- private readonly Hook _processPacketEffectResultHook;
- private readonly Hook _processPacketEffectResultBasicHook;
-
- // it's a static function of StatusManager really
- private delegate bool CancelStatusDelegate(uint statusId, uint sourceId);
- private readonly CancelStatusDelegate _cancelStatusFunc;
-
public ActionManagerEx()
{
InputOverride = new();
@@ -138,47 +89,17 @@ public ActionManagerEx()
_inst = ActionManager.Instance();
Service.Log($"[AMEx] ActionManager singleton address = 0x{(ulong)_inst:X}");
- var getGroundTargetPositionAddress = Service.SigScanner.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 44 8B 84 24 80 00 00 00 33 C0");
- _getGroundTargetPositionFunc = Marshal.GetDelegateForFunctionPointer(getGroundTargetPositionAddress);
- Service.Log($"[AMEx] GetGroundTargetPosition address = 0x{getGroundTargetPositionAddress:X}");
-
- var faceTargetAddress = Service.SigScanner.ScanText("E8 ?? ?? ?? ?? 81 FE FB 1C 00 00 74 ?? 81 FE 53 5F 00 00 74 ?? 81 FE 6F 73 00 00");
- _faceTargetFunc = Marshal.GetDelegateForFunctionPointer(faceTargetAddress);
- Service.Log($"[AMEx] FaceTarget address = 0x{faceTargetAddress:X}");
-
- _updateHook = Service.Hook.HookFromSignature("48 8B C4 48 89 58 20 57 48 81 EC", UpdateDetour);
- _updateHook.Enable();
- Service.Log($"[AMEx] Update address = 0x{_updateHook.Address:X}");
-
- _useActionLocationHook = Service.Hook.HookFromSignature("E8 ?? ?? ?? ?? 3C 01 0F 85 ?? ?? ?? ?? EB 46", UseActionLocationDetour);
- _useActionLocationHook.Enable();
- Service.Log($"[AMEx] UseActionLocation address = 0x{_useActionLocationHook.Address:X}");
-
- _useBozjaFromHolsterDirectorHook = Service.Hook.HookFromSignature("E8 ?? ?? ?? ?? 3C 01 0F 85 ?? ?? ?? ?? BD", UseBozjaFromHolsterDirectorDetour);
- _useBozjaFromHolsterDirectorHook.Enable();
- Service.Log($"[AMEx] UseBozjaFromHolsterDirector address = 0x{_useBozjaFromHolsterDirectorHook.Address:X}");
+ _updateHook = new(ActionManager.Addresses.Update, UpdateDetour);
+ _useActionLocationHook = new(ActionManager.Addresses.UseActionLocation, UseActionLocationDetour);
+ _useBozjaFromHolsterDirectorHook = new(PublicContentBozja.Addresses.UseFromHolster, UseBozjaFromHolsterDirectorDetour);
_processPacketActionEffectHook = Service.Hook.HookFromSignature("E8 ?? ?? ?? ?? 48 8B 4C 24 68 48 33 CC E8 ?? ?? ?? ?? 4C 8D 5C 24 70 49 8B 5B 20 49 8B 73 28 49 8B E3 5F C3", ProcessPacketActionEffectDetour);
_processPacketActionEffectHook.Enable();
Service.Log($"[AMEx] ProcessPacketActionEffect address = 0x{_processPacketActionEffectHook.Address:X}");
-
- _processPacketEffectResultHook = Service.Hook.HookFromSignature("48 8B C4 44 88 40 18 89 48 08", ProcessPacketEffectResultDetour);
- _processPacketEffectResultHook.Enable();
- Service.Log($"[AMEx] ProcessPacketEffectResult address = 0x{_processPacketEffectResultHook.Address:X}");
-
- _processPacketEffectResultBasicHook = Service.Hook.HookFromSignature("40 53 41 54 41 55 48 83 EC 40", ProcessPacketEffectResultBasicDetour);
- _processPacketEffectResultBasicHook.Enable();
- Service.Log($"[AMEx] ProcessPacketEffectResultBasic address = 0x{_processPacketEffectResultBasicHook.Address:X}");
-
- var cancelStatusAddress = Service.SigScanner.ScanText("E8 ?? ?? ?? ?? 84 C0 75 2C 48 8B 07");
- _cancelStatusFunc = Marshal.GetDelegateForFunctionPointer(cancelStatusAddress);
- Service.Log($"[AMEx] CancelStatus address = 0x{cancelStatusAddress:X}");
}
public void Dispose()
{
- _processPacketEffectResultBasicHook.Dispose();
- _processPacketEffectResultHook.Dispose();
_processPacketActionEffectHook.Dispose();
_useBozjaFromHolsterDirectorHook.Dispose();
_useActionLocationHook.Dispose();
@@ -189,10 +110,10 @@ public void Dispose()
public Vector3? GetWorldPosUnderCursor()
{
Vector3 res = new();
- return _getGroundTargetPositionFunc(_inst, &res) ? res : null;
+ return _inst->GetGroundPositionForCursor(&res) ? res : null;
}
- public void FaceTarget(Vector3 position, ulong unkObjID = GameObject.InvalidGameObjectId) => _faceTargetFunc(_inst, &position, unkObjID);
+ public void FaceTarget(Vector3 position, ulong unkObjID = GameObject.InvalidGameObjectId) => _inst->AutoFaceTargetPosition(&position, unkObjID);
public void FaceDirection(WDir direction)
{
var player = Service.ClientState.LocalPlayer;
@@ -255,8 +176,8 @@ public uint GetActionStatus(ActionID action, ulong target, bool checkRecastActiv
}
// returns time in ms
- public int GetAdjustedCastTime(ActionID action, bool skipHasteAdjustment = true, byte* outOptProcState = null)
- => ActionManager.GetAdjustedCastTime((FFXIVClientStructs.FFXIV.Client.Game.ActionType)action.Type, action.ID, (byte)(skipHasteAdjustment ? 1 : 0), outOptProcState);
+ public int GetAdjustedCastTime(ActionID action, bool applyProcs = true, ActionManager.CastTimeProc* outOptProc = null)
+ => ActionManager.GetAdjustedCastTime((FFXIVClientStructs.FFXIV.Client.Game.ActionType)action.Type, action.ID, applyProcs, outOptProc);
public bool IsRecastTimerActive(ActionID action)
=> _inst->IsRecastTimerActive((FFXIVClientStructs.FFXIV.Client.Game.ActionType)action.Type, action.ID);
@@ -264,33 +185,35 @@ public bool IsRecastTimerActive(ActionID action)
public int GetRecastGroup(ActionID action)
=> _inst->GetRecastGroup((int)action.Type, action.ID);
- public bool UseAction(ActionID action, ulong targetID, uint itemLocation, uint callType, uint comboRouteID, bool* outOptGTModeStarted)
- => _inst->UseAction((FFXIVClientStructs.FFXIV.Client.Game.ActionType)action.Type, action.ID, targetID, itemLocation, callType, comboRouteID, outOptGTModeStarted);
-
// skips queueing etc
- public bool UseActionRaw(ActionID action, ulong targetID = GameObject.InvalidGameObjectId, Vector3 targetPos = new(), uint itemLocation = 0)
- => UseActionLocationDetour(_inst, action.Type, action.ID, targetID, &targetPos, itemLocation);
-
- // does all the sanity checks (that status is on actor, is a buff that can be canceled, etc.)
- // on success, the status manager is updated immediately, meaning that no rate limiting is needed
- // if sourceId is not specified, removes first status with matching id
- public bool CancelStatus(uint statusId, uint sourceId = GameObject.InvalidGameObjectId)
+ private bool ExecuteAction(ActionID action, ulong targetId, Vector3 targetPos)
{
- var res = _cancelStatusFunc(statusId, sourceId);
- Service.Log($"[AMEx] Canceling status {statusId} from {sourceId:X} -> {res}");
- return res;
+ if (action.Type is ActionType.BozjaHolsterSlot0 or ActionType.BozjaHolsterSlot1)
+ {
+ // fake action type - using action from bozja holster
+ var state = PublicContentBozja.GetState(); // note: if it's non-null, the director instance can't be null too
+ var holsterIndex = state != null ? state->HolsterActions.IndexOf((byte)action.ID) : -1;
+ return holsterIndex >= 0 && PublicContentBozja.GetInstance()->UseFromHolster((uint)holsterIndex, action.Type == ActionType.BozjaHolsterSlot1 ? 1u : 0);
+ }
+ else
+ {
+ // real action type, just execute our UAL hook
+ // note that for items extraParam should be 0xFFFF (since we want to use any item, not from first inventory slot)
+ var extraParam = action.Type == ActionType.Item ? 0xFFFFu : 0;
+ return _inst->UseActionLocation((FFXIVClientStructs.FFXIV.Client.Game.ActionType)action.Type, action.ID, targetId, &targetPos, extraParam);
+ }
}
private void UpdateDetour(ActionManager* self)
{
var dt = Framework.Instance()->FrameDeltaTime;
- var imminentAction = QueueActive ? QueueAction : AutoQueue.Action;
+ var imminentAction = _inst->ActionQueued ? QueuedAction : AutoQueue.Action;
var imminentActionAdj = imminentAction.Type == ActionType.Spell ? new(ActionType.Spell, GetAdjustedActionID(imminentAction.ID)) : imminentAction;
var imminentRecast = imminentActionAdj ? _inst->GetRecastGroupDetail(GetRecastGroup(imminentActionAdj)) : null;
if (Config.RemoveCooldownDelay)
{
var cooldownOverflow = imminentRecast != null && imminentRecast->IsActive != 0 ? imminentRecast->Elapsed + dt - imminentRecast->Total : dt;
- var animlockOverflow = dt - AnimationLock;
+ var animlockOverflow = dt - _inst->AnimationLock;
_useActionInPast = Math.Min(cooldownOverflow, animlockOverflow);
if (_useActionInPast >= dt)
_useActionInPast = 0; // nothing prevented us from casting it before, so do not adjust anything...
@@ -302,7 +225,7 @@ private void UpdateDetour(ActionManager* self)
// check whether movement is safe; block movement if not and if desired
MoveMightInterruptCast &= CastTimeRemaining > 0; // previous cast could have ended without action effect
- MoveMightInterruptCast |= imminentActionAdj && CastTimeRemaining <= 0 && AnimationLock < 0.1f && GetAdjustedCastTime(imminentActionAdj) > 0 && GCD() < 0.1f; // if we're not casting, but will start soon, moving might interrupt future cast
+ MoveMightInterruptCast |= imminentActionAdj && CastTimeRemaining <= 0 && _inst->AnimationLock < 0.1f && GetAdjustedCastTime(imminentActionAdj) > 0 && GCD() < 0.1f; // if we're not casting, but will start soon, moving might interrupt future cast
bool blockMovement = Config.PreventMovingWhileCasting && MoveMightInterruptCast;
// restore rotation logic; note that movement abilities (like charge) can take multiple frames until they allow changing facing
@@ -336,13 +259,7 @@ private void UpdateDetour(ActionManager* self)
if (AutoQueue.FacingAngle != null)
FaceDirection(AutoQueue.FacingAngle.Value.ToDirection());
- var res = AutoQueue.Action.Type switch
- {
- ActionType.Item => UseActionRaw(actionAdj, targetID, AutoQueue.TargetPos, 65535),
- ActionType.BozjaHolsterSlot0 => BozjaInterop.UseFromHolster(AutoQueue.Action.As(), 0),
- ActionType.BozjaHolsterSlot1 => BozjaInterop.UseFromHolster(AutoQueue.Action.As(), 1),
- _ => UseActionRaw(actionAdj, targetID, AutoQueue.TargetPos)
- };
+ var res = ExecuteAction(actionAdj, targetID, AutoQueue.TargetPos);
//Service.Log($"[AMEx] Auto-execute {AutoQueue.Source} action {AutoQueue.Action} (=> {actionAdj}) @ {targetID:X} {Utils.Vec3String(AutoQueue.TargetPos)} => {res}");
}
else
@@ -360,22 +277,22 @@ private void UpdateDetour(ActionManager* self)
InputOverride.UnblockMovement();
}
- private bool UseActionLocationDetour(ActionManager* self, ActionType actionType, uint actionID, ulong targetID, Vector3* targetPos, uint itemLocation)
+ private bool UseActionLocationDetour(ActionManager* self, FFXIVClientStructs.FFXIV.Client.Game.ActionType actionType, uint actionId, ulong targetId, Vector3* location, uint extraParam)
{
var pc = Service.ClientState.LocalPlayer;
- var prevSeq = LastUsedActionSequence;
+ var prevSeq = _inst->LastUsedActionSequence;
var prevRot = pc?.Rotation ?? 0;
- bool ret = _useActionLocationHook.Original(self, actionType, actionID, targetID, targetPos, itemLocation);
- var currSeq = LastUsedActionSequence;
+ bool ret = _useActionLocationHook.Original(self, actionType, actionId, targetId, location, extraParam);
+ var currSeq = _inst->LastUsedActionSequence;
var currRot = pc?.Rotation ?? 0;
if (currSeq != prevSeq)
{
- HandleActionRequest(new(actionType, actionID), currSeq, targetID, *targetPos, prevRot, currRot);
+ HandleActionRequest(new((ActionType)actionType, actionId), currSeq, targetId, *location, prevRot, currRot);
}
return ret;
}
- private bool UseBozjaFromHolsterDirectorDetour(void* self, uint holsterIndex, uint slot)
+ private bool UseBozjaFromHolsterDirectorDetour(PublicContentBozja* self, uint holsterIndex, uint slot)
{
var pc = Service.ClientState.LocalPlayer;
var prevRot = pc?.Rotation ?? 0;
@@ -383,7 +300,7 @@ private bool UseBozjaFromHolsterDirectorDetour(void* self, uint holsterIndex, ui
if (res)
{
var currRot = pc?.Rotation ?? 0;
- var entry = BozjaInterop.GetHolsterEntry(holsterIndex);
+ var entry = (BozjaHolsterID)self->State.HolsterActions[(int)holsterIndex];
HandleActionRequest(ActionID.MakeBozjaHolster(entry, (int)slot), 0, GameObject.InvalidGameObjectId, default, prevRot, currRot);
}
return res;
@@ -417,9 +334,9 @@ private void ProcessPacketActionEffectDetour(uint casterID, FFXIVClientStructs.F
}
ActionEffectReceived.Fire(casterID, info);
- var prevAnimLock = AnimationLock;
+ var prevAnimLock = _inst->AnimationLock;
_processPacketActionEffectHook.Original(casterID, casterObj, targetPos, header, effects, targets);
- var currAnimLock = AnimationLock;
+ var currAnimLock = _inst->AnimationLock;
if (casterID != Service.ClientState.LocalPlayer?.ObjectId || header->SourceSequence == 0 && _lastReqSequence != 0)
{
@@ -453,7 +370,7 @@ private void ProcessPacketActionEffectDetour(uint casterID, FFXIVClientStructs.F
{
animLockReduction = Math.Min(adjDelay - AnimationLockDelayMax, currAnimLock);
adjDelay -= animLockReduction;
- Utils.WriteField(_inst, 8, currAnimLock - animLockReduction);
+ _inst->AnimationLock = currAnimLock - animLockReduction;
}
}
AnimationLockDelayAverage = adjDelay * (1 - AnimationLockDelaySmoothing) + AnimationLockDelayAverage * AnimationLockDelaySmoothing;
@@ -468,33 +385,9 @@ private void ProcessPacketActionEffectDetour(uint casterID, FFXIVClientStructs.F
_lastReqSequence = -1;
}
- private void ProcessPacketEffectResultDetour(uint targetID, byte* packet, byte replaying)
- {
- var count = packet[0];
- var p = (Network.ServerIPC.EffectResultEntry*)(packet + 4);
- for (int i = 0; i < count; ++i)
- {
- EffectResultReceived.Fire(targetID, p->RelatedActionSequence, p->RelatedTargetIndex);
- ++p;
- }
- _processPacketEffectResultHook.Original(targetID, packet, replaying);
- }
-
- private void ProcessPacketEffectResultBasicDetour(uint targetID, byte* packet, byte replaying)
- {
- var count = packet[0];
- var p = (Network.ServerIPC.EffectResultBasicEntry*)(packet + 4);
- for (int i = 0; i < count; ++i)
- {
- EffectResultReceived.Fire(targetID, p->RelatedActionSequence, p->RelatedTargetIndex);
- ++p;
- }
- _processPacketEffectResultBasicHook.Original(targetID, packet, replaying);
- }
-
private void HandleActionRequest(ActionID action, int seq, ulong targetID, Vector3 targetPos, float prevRot, float currRot)
{
- _lastReqInitialAnimLock = AnimationLock;
+ _lastReqInitialAnimLock = _inst->AnimationLock;
_lastReqSequence = seq;
MoveMightInterruptCast = CastTimeRemaining > 0;
if (prevRot != currRot && Config.RestoreRotation)
@@ -508,9 +401,9 @@ private void HandleActionRequest(ActionID action, int seq, ulong targetID, Vecto
if (_useActionInPast > 0)
{
if (CastTimeRemaining > 0)
- Utils.WriteField(_inst, 0x30, CastTimeElapsed + _useActionInPast);
+ _inst->CastTimeElapsed += _useActionInPast;
else
- Utils.WriteField(_inst, 8, Math.Max(0, AnimationLock - _useActionInPast));
+ _inst->AnimationLock = Math.Max(0, _inst->AnimationLock - _useActionInPast);
if (recast != null)
recast->Elapsed += _useActionInPast;
@@ -518,7 +411,7 @@ private void HandleActionRequest(ActionID action, int seq, ulong targetID, Vecto
var recastElapsed = recast != null ? recast->Elapsed : 0;
var recastTotal = recast != null ? recast->Total : 0;
- Service.Log($"[AMEx] UAL #{seq} {action} @ {targetID:X} / {Utils.Vec3String(targetPos)}, ALock={AnimationLock:f3}, CTR={CastTimeRemaining:f3}, CD={recastElapsed:f3}/{recastTotal:f3}, GCD={GCD():f3}");
- ActionRequested.Fire(new(action, targetID, targetPos, (uint)seq, AnimationLock, CastSpellID != 0 ? CastTimeElapsed : 0, CastSpellID != 0 ? CastTimeTotal : 0, recastElapsed, recastTotal));
+ Service.Log($"[AMEx] UAL #{seq} {action} @ {targetID:X} / {Utils.Vec3String(targetPos)}, ALock={_inst->AnimationLock:f3}, CTR={CastTimeRemaining:f3}, CD={recastElapsed:f3}/{recastTotal:f3}, GCD={GCD():f3}");
+ ActionRequested.Fire(new(action, targetID, targetPos, (uint)seq, _inst->AnimationLock, _inst->CastSpellId != 0 ? _inst->CastTimeElapsed : 0, _inst->CastSpellId != 0 ? _inst->CastTimeTotal : 0, recastElapsed, recastTotal));
}
}
diff --git a/BossMod/Framework/BozjaInterop.cs b/BossMod/Framework/BozjaInterop.cs
deleted file mode 100644
index 78068e170d..0000000000
--- a/BossMod/Framework/BozjaInterop.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using System.Runtime.InteropServices;
-
-namespace BossMod;
-
-// bozja-specific utilities
-unsafe sealed class BozjaInterop : IDisposable
-{
- public static BozjaInterop? Instance;
-
- [StructLayout(LayoutKind.Explicit)]
- private struct Holster
- {
- public const int Capacity = 100;
-
- [FieldOffset(0x6C)] public fixed byte Contents[Capacity];
- }
-
- private delegate Holster* GetHolsterDelegate();
- private readonly GetHolsterDelegate _getHolsterFunc;
-
- private delegate bool UseFromHolsterDelegate(uint holsterId, uint slot);
- private readonly UseFromHolsterDelegate _useFromHolsterFunc;
-
- public BozjaInterop()
- {
- var getHolsterAddress = Service.SigScanner.ScanText("E8 ?? ?? ?? ?? 48 85 FF 74 1D");
- _getHolsterFunc = Marshal.GetDelegateForFunctionPointer(getHolsterAddress);
- Service.Log($"[BozjaInterop] GetHolster address = 0x{getHolsterAddress:X}");
-
- var useFromHolsterAddress = Service.SigScanner.ScanText("E8 ?? ?? ?? ?? 48 8B 47 38 89 70 18");
- _useFromHolsterFunc = Marshal.GetDelegateForFunctionPointer(useFromHolsterAddress);
- Service.Log($"[BozjaInterop] UseFromHolster address = 0x{useFromHolsterAddress:X}");
- }
-
- public void Dispose()
- {
- }
-
- public static void FetchHolster(Span result)
- {
- if (result.Length < (int)BozjaHolsterID.Count)
- throw new ArgumentException($"Buffer too small: {result.Length} < {(int)BozjaHolsterID.Count}");
-
- result.Clear();
- var holster = Instance != null ? Instance._getHolsterFunc() : null;
- if (holster != null)
- {
- for (int i = 0; i < Holster.Capacity; ++i)
- {
- var entry = holster->Contents[i];
- if (entry != 0)
- ++result[entry];
- }
- }
- }
-
- public static BozjaHolsterID GetHolsterEntry(uint index)
- {
- var holster = Instance != null ? Instance._getHolsterFunc() : null;
- return holster != null ? (BozjaHolsterID)holster->Contents[index] : BozjaHolsterID.None;
- }
-
- public static bool UseFromHolster(BozjaHolsterID id, uint slot) => Instance?._useFromHolsterFunc((uint)id, slot) ?? false;
-}
diff --git a/BossMod/Framework/Countdown.cs b/BossMod/Framework/Countdown.cs
deleted file mode 100644
index 71b3086426..0000000000
--- a/BossMod/Framework/Countdown.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using FFXIVClientStructs.FFXIV.Client.System.Framework;
-using FFXIVClientStructs.FFXIV.Client.UI.Agent;
-using System.Runtime.InteropServices;
-
-namespace BossMod;
-
-[StructLayout(LayoutKind.Explicit)]
-public unsafe struct Countdown
-{
- [FieldOffset(0x28)] public float Timer;
- [FieldOffset(0x38)] public byte Active;
- [FieldOffset(0x3C)] public uint Initiator;
-
- public static unsafe Countdown* Instance => (Countdown*)Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.CountDownSettingDialog);
-
- public static float? TimeRemaining()
- {
- var inst = Instance;
- return inst->Active != 0 ? inst->Timer : null;
- }
-}
diff --git a/BossMod/Framework/PartyAlliance.cs b/BossMod/Framework/PartyAlliance.cs
deleted file mode 100644
index b1e13230d9..0000000000
--- a/BossMod/Framework/PartyAlliance.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using FFXIVClientStructs.FFXIV.Client.Game.Group;
-
-namespace BossMod;
-
-// similar to dalamud's PartyList, except that it works with alliances properly
-class PartyAlliance
-{
- public static int MaxAllianceMembers = 20;
-
- private readonly unsafe GroupManager* _groupManager = GroupManager.Instance();
-
- public unsafe int NumPartyMembers => _groupManager->MemberCount;
- public unsafe bool IsAlliance => (_groupManager->AllianceFlags & 1) != 0;
- public unsafe bool IsSmallGroupAlliance => (_groupManager->AllianceFlags & 2) != 0; // alliance containing 6 groups of 4 members rather than 3x8
-
- public unsafe PartyMember* PartyMember(int index) => (index >= 0 && index < NumPartyMembers) ? ArrayElement(_groupManager->PartyMembers, index) : null;
- public unsafe PartyMember* AllianceMember(int rawIndex) => (rawIndex is >= 0 and < 20) ? AllianceMemberIfValid(rawIndex) : null;
- public unsafe PartyMember* AllianceMember(int group, int index)
- {
- if (IsSmallGroupAlliance)
- return group is >= 0 and < 5 && index is >= 0 and < 4 ? AllianceMemberIfValid(4 * group + index) : null;
- else
- return group is >= 0 and < 2 && index is >= 0 and < 8 ? AllianceMemberIfValid(8 * group + index) : null;
- }
-
- public unsafe PartyMember* FindPartyMember(ulong contentID)
- {
- for (int i = 0; i < NumPartyMembers; ++i)
- {
- var m = ArrayElement(_groupManager->PartyMembers, i);
- if ((ulong)m->ContentID == contentID)
- return m;
- }
- return null;
- }
-
- private static unsafe PartyMember* ArrayElement(byte* array, int index) => ((PartyMember*)array) + index;
- private unsafe PartyMember* AllianceMemberIfValid(int rawIndex)
- {
- var p = ArrayElement(_groupManager->AllianceMembers, rawIndex);
- return (p->Flags & 1) != 0 ? p : null;
- }
-}
diff --git a/BossMod/Framework/Plugin.cs b/BossMod/Framework/Plugin.cs
index b227ed8b19..1c555b46f3 100644
--- a/BossMod/Framework/Plugin.cs
+++ b/BossMod/Framework/Plugin.cs
@@ -30,14 +30,20 @@ public sealed class Plugin : IDalamudPlugin
private readonly ReplayManagementWindow _wndReplay;
private readonly MainDebugWindow _wndDebug;
- public Plugin(
+ public unsafe Plugin(
[RequiredVersion("1.0")] DalamudPluginInterface dalamud,
[RequiredVersion("1.0")] ICommandManager commandManager)
{
+ if (!dalamud.ConfigDirectory.Exists)
+ dalamud.ConfigDirectory.Create();
var dalamudRoot = dalamud.GetType().Assembly.
GetType("Dalamud.Service`1", true)!.MakeGenericType(dalamud.GetType().Assembly.GetType("Dalamud.Dalamud", true)!).
GetMethod("Get")!.Invoke(null, BindingFlags.Default, null, [], null);
var dalamudStartInfo = dalamudRoot?.GetType().GetProperty("StartInfo", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(dalamudRoot) as DalamudStartInfo;
+ var gameVersion = dalamudStartInfo?.GameVersion?.ToString() ?? "unknown";
+ InteropGenerator.Runtime.Resolver.GetInstance.Setup(0, gameVersion, new(dalamud.ConfigDirectory.FullName + "/cs.json"));
+ FFXIVClientStructs.Interop.Generated.Addresses.Register();
+ InteropGenerator.Runtime.Resolver.GetInstance.Resolve();
dalamud.Create();
Service.LogHandler = (string msg) => Service.Logger.Debug(msg);
@@ -53,13 +59,13 @@ public Plugin(
Service.Config.LoadFromFile(dalamud.ConfigFile);
Service.Config.Modified.Subscribe(() => Service.Config.SaveToFile(dalamud.ConfigFile));
- BozjaInterop.Instance = new();
ActionManagerEx.Instance = new(); // needs config
CommandManager = commandManager;
CommandManager.AddHandler("/vbm", new CommandInfo(OnCommand) { HelpMessage = "Show boss mod config UI" });
- _ws = new(Utils.FrameQPF(), dalamudStartInfo?.GameVersion?.ToString() ?? "unknown");
+ var qpf = (ulong)FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->PerformanceCounterFrequency;
+ _ws = new(qpf, gameVersion);
_wsSync = new(_ws);
_bossmod = new(_ws);
_autorotation = new(_bossmod);
@@ -92,7 +98,6 @@ public void Dispose()
_autorotation.Dispose();
_wsSync.Dispose();
ActionManagerEx.Instance?.Dispose();
- BozjaInterop.Instance?.Dispose();
CommandManager.RemoveHandler("/vbm");
}
diff --git a/BossMod/Framework/Utils.cs b/BossMod/Framework/Utils.cs
index 5e3d3e16c8..547c3b98e5 100644
--- a/BossMod/Framework/Utils.cs
+++ b/BossMod/Framework/Utils.cs
@@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.Objects.Types;
+using JetBrains.Annotations;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
@@ -42,42 +43,17 @@ public static string ObjectKindString(GameObject obj)
public static unsafe T ReadField(void* address, int offset) where T : unmanaged => *(T*)((IntPtr)address + offset);
public static unsafe void WriteField(void* address, int offset, T value) where T : unmanaged => *(T*)((IntPtr)address + offset) = value;
- private unsafe delegate byte GameObjectIsFriendlyDelegate(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* obj);
- private static readonly GameObjectIsFriendlyDelegate GameObjectIsFriendlyFunc = Marshal.GetDelegateForFunctionPointer(Service.SigScanner.ScanText("E8 ?? ?? ?? ?? 33 C9 84 C0 0F 95 C1 8D 41 03"));
+ public unsafe delegate byte GameObjectIsFriendlyDelegate(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* obj);
+ public static readonly GameObjectIsFriendlyDelegate GameObjectIsFriendly = Marshal.GetDelegateForFunctionPointer(Service.SigScanner.ScanText("E8 ?? ?? ?? ?? 33 C9 84 C0 0F 95 C1 8D 41 03"));
public static unsafe FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* GameObjectInternal(GameObject? obj) => obj != null ? (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address : null;
public static unsafe FFXIVClientStructs.FFXIV.Client.Game.Character.Character* CharacterInternal(Character? chr) => chr != null ? (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)chr.Address : null;
public static unsafe FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara* BattleCharaInternal(BattleChara? chara) => chara != null ? (FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)chara.Address : null;
- public static unsafe bool GameObjectIsDead(GameObject obj) => GameObjectInternal(obj)->IsDead();
- public static unsafe bool GameObjectIsTargetable(GameObject obj) => GameObjectInternal(obj)->GetIsTargetable();
- public static unsafe bool GameObjectIsFriendly(GameObject obj) => GameObjectIsFriendlyFunc(GameObjectInternal(obj)) != 0;
- public static unsafe byte GameObjectEventState(GameObject obj) => ReadField(GameObjectInternal(obj), 0x70); // see actor control 106
- public static unsafe float GameObjectRadius(GameObject obj) => GameObjectInternal(obj)->GetRadius();
- public static unsafe uint GameObjectFateID(GameObject obj) => GameObjectInternal(obj)->FateId;
- //public static unsafe Vector3 GameObjectNonInterpolatedPosition(GameObject obj) => ReadField(GameObjectInternal(obj), 0x10);
- //public static unsafe float GameObjectNonInterpolatedRotation(GameObject obj) => ReadField(GameObjectInternal(obj), 0x20);
- public static unsafe byte CharacterShieldValue(Character chr) => ReadField(CharacterInternal(chr), 0x1A0 + 0x46); // CharacterInternal(chr)->ShieldValue; // % of max hp; see effect result
- public static unsafe bool CharacterInCombat(Character chr) => (ReadField(CharacterInternal(chr), 0x1EB) & 0x20) != 0; // see actor control 4
- public static unsafe byte CharacterAnimationState(Character chr, bool second) => ReadField(CharacterInternal(chr), 0x970 + (second ? 0x2C2 : 0x2C1)); // see actor control 62
- public static unsafe byte CharacterModelState(Character chr) => ReadField(CharacterInternal(chr), 0x970 + 0x2C0); // see actor control 63
- public static unsafe float CharacterCastRotation(Character chr) => ReadField(CharacterInternal(chr), 0x1B6C); // see ActorCast -> Character::StartCast
- public static unsafe ulong CharacterTargetID(Character chr) => ReadField(CharacterInternal(chr), 0x1B58); // until FFXIVClientStructs fixes offset and type...
- public static unsafe ushort CharacterTetherID(Character chr) => ReadField(CharacterInternal(chr), 0x12F0 + 0xA0); // see actor control 35 -> CharacterTethers::Set (note that there is also a secondary tether...)
- public static unsafe ulong CharacterTetherTargetID(Character chr) => ReadField(CharacterInternal(chr), 0x12F0 + 0xA0 + 0x10);
- public static unsafe Vector3 BattleCharaCastLocation(BattleChara chara) => BattleCharaInternal(chara)->GetCastInfo->CastLocation; // see ActorCast -> Character::StartCast -> Character::StartOmen
-
- public static unsafe uint FrameIndex() => FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->FrameCounter;
- public static unsafe ulong FrameQPF() => ReadField(FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(), 0x16A0);
- public static unsafe ulong FrameQPC() => ReadField(FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(), 0x16A8);
- public static unsafe float FrameDuration() => FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->FrameDeltaTime;
- public static unsafe float FrameDurationRaw() => ReadField(FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(), 0x16BC);
- public static unsafe float TickSpeedMultiplier() => ReadField(FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(), 0x17B0);
-
public static unsafe ulong MouseoverID()
{
var pronoun = FFXIVClientStructs.FFXIV.Client.UI.Misc.PronounModule.Instance();
- return pronoun != null && pronoun->UiMouseOverTarget != null ? pronoun->UiMouseOverTarget->ObjectID : 0;
+ return pronoun != null && pronoun->UiMouseOverTarget != null ? pronoun->UiMouseOverTarget->EntityId : 0;
}
public static unsafe ulong SceneObjectFlags(FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object* o) => ReadField(o, 0x38);
diff --git a/BossMod/Framework/WorldStateGameSync.cs b/BossMod/Framework/WorldStateGameSync.cs
index abadc33356..079e69bcdc 100644
--- a/BossMod/Framework/WorldStateGameSync.cs
+++ b/BossMod/Framework/WorldStateGameSync.cs
@@ -1,23 +1,31 @@
-using Dalamud.Game.ClientState.Objects.Types;
-using Dalamud.Hooking;
+using Dalamud.Hooking;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Fate;
+using FFXIVClientStructs.FFXIV.Client.Game.Group;
+using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
+using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using FFXIVClientStructs.FFXIV.Client.System.Framework;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.Interop;
namespace BossMod;
// utility that updates a world state to correspond to game state
sealed class WorldStateGameSync : IDisposable
{
+ private const int ObjectTableSize = 599; // should match CS; note that different ranges are used for different purposes - consider splitting?..
+ private const uint InvalidEntityId = 0xE0000000;
+
private readonly WorldState _ws;
private readonly DateTime _startTime;
- private readonly ulong _startQPC;
+ private readonly long _startQPC;
- private readonly PartyAlliance _alliance = new();
private readonly List _globalOps = [];
private readonly Dictionary> _actorOps = [];
- private readonly Actor?[] _actorsByIndex = new Actor?[Service.ObjectTable.Length];
+ private readonly Actor?[] _actorsByIndex = new Actor?[ObjectTableSize];
private readonly List<(ulong Caster, ActorCastEvent Event)> _castEvents = [];
private readonly List<(uint Seq, ulong Target, int TargetIndex)> _confirms = [];
@@ -29,6 +37,10 @@ sealed class WorldStateGameSync : IDisposable
private readonly ConfigListener _netConfig;
private readonly EventSubscriptions _subscriptions;
+ private unsafe delegate void ProcessPacketEffectResultDelegate(uint targetID, byte* packet, byte replaying);
+ private readonly Hook _processPacketEffectResultHook;
+ private readonly Hook _processPacketEffectResultBasicHook;
+
private delegate void ProcessPacketActorControlDelegate(uint actorID, uint category, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, ulong targetID, byte replaying);
private readonly Hook _processPacketActorControlHook;
@@ -45,17 +57,24 @@ public unsafe WorldStateGameSync(WorldState ws)
{
_ws = ws;
_startTime = DateTime.Now;
- _startQPC = Utils.FrameQPC();
+ _startQPC = Framework.Instance()->PerformanceCounterValue;
_interceptor.ServerIPCReceived += ServerIPCReceived;
_netConfig = Service.Config.GetAndSubscribe(config => _interceptor.Active = config.RecordServerPackets || config.DumpServerPackets);
_subscriptions = new
(
ActionManagerEx.Instance!.ActionRequested.Subscribe(OnActionRequested),
- ActionManagerEx.Instance!.ActionEffectReceived.Subscribe(OnActionEffect),
- ActionManagerEx.Instance!.EffectResultReceived.Subscribe(OnEffectResult)
+ ActionManagerEx.Instance!.ActionEffectReceived.Subscribe(OnActionEffect)
);
+ _processPacketEffectResultHook = Service.Hook.HookFromSignature("48 8B C4 44 88 40 18 89 48 08", ProcessPacketEffectResultDetour);
+ _processPacketEffectResultHook.Enable();
+ Service.Log($"[WSG] ProcessPacketEffectResult address = 0x{_processPacketEffectResultHook.Address:X}");
+
+ _processPacketEffectResultBasicHook = Service.Hook.HookFromSignature("40 53 41 54 41 55 48 83 EC 40", ProcessPacketEffectResultBasicDetour);
+ _processPacketEffectResultBasicHook.Enable();
+ Service.Log($"[WSG] ProcessPacketEffectResultBasic address = 0x{_processPacketEffectResultBasicHook.Address:X}");
+
_processPacketActorControlHook = Service.Hook.HookFromSignature("E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64", ProcessPacketActorControlDetour);
_processPacketActorControlHook.Enable();
Service.Log($"[WSG] ProcessPacketActorControl address = 0x{_processPacketActorControlHook.Address:X}");
@@ -76,6 +95,8 @@ public unsafe WorldStateGameSync(WorldState ws)
public void Dispose()
{
+ _processPacketEffectResultBasicHook.Dispose();
+ _processPacketEffectResultHook.Dispose();
_processPacketActorControlHook.Dispose();
_processPacketNpcYellHook.Dispose();
_processEnvControlHook.Dispose();
@@ -87,15 +108,16 @@ public void Dispose()
public unsafe void Update(TimeSpan prevFramePerf)
{
+ var fwk = Framework.Instance();
_ws.Execute(new WorldState.OpFrameStart
(
new(
- _startTime.AddSeconds((double)(Utils.FrameQPC() - _startQPC) / _ws.QPF),
- Utils.FrameQPC(),
- Utils.FrameIndex(),
- Utils.FrameDurationRaw(),
- Utils.FrameDuration(),
- Utils.TickSpeedMultiplier()
+ _startTime.AddSeconds((double)(fwk->PerformanceCounterValue - _startQPC) / _ws.QPF),
+ (ulong)fwk->PerformanceCounterValue,
+ fwk->FrameCounter,
+ fwk->RealFrameDeltaTime,
+ fwk->FrameDeltaTime,
+ fwk->GameSpeedMultiplier
),
prevFramePerf,
GaugeData()
@@ -132,7 +154,7 @@ public unsafe void Update(TimeSpan prevFramePerf)
private unsafe void UpdateWaymarks()
{
var wm = Waymark.A;
- foreach (ref var marker in MarkingController.Instance()->FieldMarkerArraySpan)
+ foreach (ref var marker in MarkingController.Instance()->FieldMarkers)
{
Vector3? pos = marker.Active ? new(marker.X / 1000.0f, marker.Y / 1000.0f, marker.Z / 1000.0f) : null;
if (_ws.Waymarks[wm] != pos)
@@ -141,23 +163,24 @@ private unsafe void UpdateWaymarks()
}
}
- private void UpdateActors()
+ private unsafe void UpdateActors()
{
+ var mgr = GameObjectManager.Instance();
for (int i = 0; i < _actorsByIndex.Length; ++i)
{
var actor = _actorsByIndex[i];
- var obj = Service.ObjectTable[i];
+ var obj = mgr->Objects.All[i].Value;
- if (obj != null && obj.ObjectId == GameObject.InvalidGameObjectId)
+ if (obj != null && obj->EntityId == InvalidEntityId)
obj = null; // ignore non-networked objects (really?..)
- if (obj != null && (obj.ObjectId & 0xFF000000) == 0xFF000000)
+ if (obj != null && (obj->EntityId & 0xFF000000) == 0xFF000000)
{
- //Service.Log($"[WorldState] Skipping bad object #{i} with id {obj.ObjectId:X}");
+ Service.Log($"[WorldState] Skipping bad object #{i} with id {obj->EntityId:X}");
obj = null;
}
- if (actor != null && actor.InstanceID != obj?.ObjectId)
+ if (actor != null && (obj == null || actor.InstanceID != obj->EntityId))
{
_actorsByIndex[i] = null;
RemoveActor(actor);
@@ -166,7 +189,7 @@ private void UpdateActors()
if (obj != null)
{
- if (actor != _ws.Actors.Find(obj.ObjectId))
+ if (actor != _ws.Actors.Find(obj->EntityId))
{
Service.Log($"[WorldState] Actor position mismatch for #{i} {actor}");
}
@@ -185,40 +208,40 @@ private void RemoveActor(Actor actor)
_ws.Execute(new ActorState.OpDestroy(actor.InstanceID));
}
- private void UpdateActor(GameObject obj, int index, Actor? act)
+ private unsafe void UpdateActor(GameObject* obj, int index, Actor? act)
{
- var character = obj as Character;
- var name = obj.Name.TextValue;
- var nameID = character?.NameId ?? 0;
- var classID = (Class)(character?.ClassJob.Id ?? 0);
- var level = character?.Level ?? 0;
- var posRot = new Vector4(obj.Position, obj.Rotation);
+ var chr = obj->IsCharacter() ? (Character*)obj : null;
+ var name = obj->NameString;
+ var nameID = chr != null ? chr->NameId : 0;
+ var classID = chr != null ? (Class)chr->ClassJob : Class.None;
+ var level = chr != null ? chr->Level : 0;
+ var posRot = new Vector4(obj->Position, obj->Rotation);
var hpmp = new ActorHPMP();
bool inCombat = false;
- if (character != null)
+ if (chr != null)
{
- hpmp.CurHP = character.CurrentHp;
- hpmp.MaxHP = character.MaxHp;
- hpmp.Shield = (uint)(Utils.CharacterShieldValue(character) * 0.01f * hpmp.MaxHP);
- hpmp.CurMP = character.CurrentMp;
- inCombat = Utils.CharacterInCombat(character);
+ hpmp.CurHP = chr->Health;
+ hpmp.MaxHP = chr->MaxHealth;
+ hpmp.Shield = (uint)(chr->ShieldValue * 0.01f * hpmp.MaxHP);
+ hpmp.CurMP = chr->Mana;
+ inCombat = chr->InCombat;
}
- var targetable = Utils.GameObjectIsTargetable(obj);
- var friendly = Utils.GameObjectIsFriendly(obj);
- var isDead = Utils.GameObjectIsDead(obj);
- var target = character == null ? 0 : SanitizedObjectID(obj != Service.ClientState.LocalPlayer ? Utils.CharacterTargetID(character) : (Service.TargetManager.Target?.ObjectId ?? 0)); // this is a bit of a hack - when changing targets, we want AI to see changes immediately rather than wait for server response
- var modelState = character != null ? new ActorModelState(Utils.CharacterModelState(character), Utils.CharacterAnimationState(character, false), Utils.CharacterAnimationState(character, true)) : default;
- var eventState = Utils.GameObjectEventState(obj);
- var radius = Utils.GameObjectRadius(obj);
+ var targetable = obj->GetIsTargetable();
+ var friendly = Utils.GameObjectIsFriendly(obj) != 0;
+ var isDead = obj->IsDead();
+ var target = chr != null ? SanitizedObjectID(chr->GetTargetId()) : 0; // note: when changing targets, we want to see changes immediately rather than wait for server response
+ var modelState = chr != null ? new ActorModelState(chr->Timeline.ModelState, chr->Timeline.AnimationState[0], chr->Timeline.AnimationState[1]) : default;
+ var eventState = obj->EventState;
+ var radius = obj->GetRadius();
if (act == null)
{
- var type = (ActorType)(((int)obj.ObjectKind << 8) + obj.SubKind);
- _ws.Execute(new ActorState.OpCreate(obj.ObjectId, obj.DataId, index, name, nameID, type, classID, level, posRot, radius, hpmp, targetable, friendly, SanitizedObjectID(obj.OwnerId), Utils.GameObjectFateID(obj)));
- act = _actorsByIndex[index] = _ws.Actors.Find(obj.ObjectId)!;
+ var type = (ActorType)(((int)obj->ObjectKind << 8) + obj->SubKind);
+ _ws.Execute(new ActorState.OpCreate(obj->EntityId, obj->BaseId, index, name, nameID, type, classID, level, posRot, radius, hpmp, targetable, friendly, SanitizedObjectID(obj->OwnerId), obj->FateId));
+ act = _actorsByIndex[index] = _ws.Actors.Find(obj->EntityId)!;
// note: for now, we continue relying on network messages for tether changes, since sometimes multiple changes can happen in a single frame, and some components rely on seeing all of them...
- var tether = character != null ? new ActorTetherInfo(Utils.CharacterTetherID(character), Utils.CharacterTetherTargetID(character)) : default;
+ var tether = chr != null ? new ActorTetherInfo(chr->Vfx.Tethers[0].Id, chr->Vfx.Tethers[0].TargetId) : default;
if (tether.ID != 0)
_ws.Execute(new ActorState.OpTether(act.InstanceID, tether));
}
@@ -252,29 +275,33 @@ private void UpdateActor(GameObject obj, int index, Actor? act)
_ws.Execute(new ActorState.OpTarget(act.InstanceID, target));
DispatchActorEvents(act.InstanceID);
- var chara = obj as BattleChara;
- if (chara != null)
+ var castInfo = chr != null ? chr->GetCastInfo() : null;
+ if (castInfo != null)
{
- var curCast = chara.IsCasting
+ var curCast = castInfo->IsCasting != 0
? new ActorCastInfo
{
- Action = new((ActionType)chara.CastActionType, chara.CastActionId),
- TargetID = SanitizedObjectID(chara.CastTargetObjectId),
- Rotation = Utils.CharacterCastRotation(chara).Radians(),
- Location = Utils.BattleCharaCastLocation(chara),
- TotalTime = chara.TotalCastTime,
- FinishAt = _ws.CurrentTime.AddSeconds(Math.Clamp(chara.TotalCastTime - chara.CurrentCastTime, 0, 100000)),
- Interruptible = chara.IsCastInterruptible
+ Action = new((ActionType)castInfo->ActionType, castInfo->ActionId),
+ TargetID = SanitizedObjectID(castInfo->CastTargetId),
+ Rotation = chr->CastRotation.Radians(),
+ Location = castInfo->CastLocation,
+ TotalTime = castInfo->TotalCastTime, // TODO: should it use adjusted here?..
+ FinishAt = _ws.CurrentTime.AddSeconds(Math.Clamp(castInfo->TotalCastTime - castInfo->CurrentCastTime, 0, 100000)),
+ Interruptible = castInfo->Interruptible != 0,
} : null;
UpdateActorCastInfo(act, curCast);
+ }
- for (int i = 0; i < chara.StatusList.Length; ++i)
+ var sm = chr != null ? chr->GetStatusManager() : null;
+ if (sm != null)
+ {
+ for (int i = 0; i < sm->NumValidStatuses; ++i)
{
// note: sometimes (Ocean Fishing) remaining-time is weird (I assume too large?) and causes exception in AddSeconds - so we just clamp it to some reasonable range
// note: self-cast buffs with duration X will have duration -X until EffectResult (~0.6s later); see autorotation for more details
ActorStatus curStatus = new();
- var s = chara.StatusList[i];
- if (s != null && s.StatusId != 0)
+ ref var s = ref sm->Status[i];
+ if (s.StatusId != 0)
{
var dur = Math.Min(Math.Abs(s.RemainingTime), 100000);
curStatus.ID = s.StatusId;
@@ -322,54 +349,90 @@ private void UpdateActorStatus(Actor act, int index, ActorStatus value)
private unsafe void UpdateParty()
{
+ var gm = GroupManager.Instance();
+ var ui = UIState.Instance();
+
// update player slot
- UpdatePartySlot(PartyState.PlayerSlot, Service.ClientState.LocalContentId, Service.ClientState.LocalPlayer?.ObjectId ?? 0);
+ UpdatePartySlot(PartyState.PlayerSlot, UIState.Instance()->PlayerState.ContentId, UIState.Instance()->PlayerState.ObjectId);
// update normal party slots: first update/remove existing members, then add new ones
for (int i = PartyState.PlayerSlot + 1; i < PartyState.MaxPartySize; ++i)
{
var contentID = _ws.Party.ContentIDs[i];
- if (contentID == 0)
- continue; // skip empty slots
-
- var member = _alliance.FindPartyMember(contentID);
- if (member == null)
- UpdatePartySlot(i, 0, 0);
- else
- UpdatePartySlot(i, contentID, member->ObjectID);
- }
- for (int i = 0; i < _alliance.NumPartyMembers; ++i)
- {
- var member = _alliance.PartyMember(i);
- if (member == null)
- continue;
-
- var contentID = (ulong)member->ContentID;
- if (_ws.Party.ContentIDs.IndexOf(contentID) != -1)
- continue; // already added, updated in previous loop
-
- var freeSlot = _ws.Party.ContentIDs[1..].IndexOf(0ul);
- if (freeSlot == -1)
+ var instanceID = _ws.Party.ActorIDs[i];
+ if (contentID != 0)
{
- Service.Log($"[WorldState] Failed to find empty slot for party member {contentID:X}:{member->ObjectID:X}");
- continue;
+ // slot was occupied by player => see if it's still in party
+ var member = gm->GetPartyMemberByContentId(contentID);
+ if (member != null)
+ UpdatePartySlot(i, contentID, member->ObjectId); // slot is still occupied by player; update in case instance-id changed
+ else
+ UpdatePartySlot(i, 0, 0); // player is no longer in party => clear slot
}
-
- UpdatePartySlot(freeSlot + 1, contentID, member->ObjectID);
+ else if (instanceID != 0)
+ {
+ // slot was occupied by trust => see if it's still in party
+ if (!HasBuddy(instanceID))
+ UpdatePartySlot(i, 0, 0); // buddy is no longer in party => clear slot
+ // else: no reason to update...
+ }
+ // else: slot was empty, skip
+ }
+ for (int i = 0; i < gm->MemberCount; ++i)
+ {
+ ref var member = ref gm->PartyMembers[i];
+ if (_ws.Party.ContentIDs.IndexOf(member.ContentId) == -1)
+ AddPartyMember(member.ContentId, member.ObjectId);
+ // else: already added, updated in previous loop
+ }
+ for (int i = 0; i < ui->Buddy.DutyHelperInfo.ENpcIds.Length; ++i)
+ {
+ var instanceID = ui->Buddy.DutyHelperInfo.DutyHelpers[i].ObjectId;
+ if (instanceID != InvalidEntityId && _ws.Party.ActorIDs[1..PartyState.MaxPartySize].IndexOf(instanceID) == -1)
+ AddPartyMember(0, instanceID);
+ // else: buddy is non-existent or already updated, skip
}
// update alliance members
+ var isNormalAlliance = gm->IsAlliance && !gm->IsSmallGroupAlliance;
for (int i = PartyState.MaxPartySize; i < PartyState.MaxAllianceSize; ++i)
{
- var member = _alliance.IsAlliance && !_alliance.IsSmallGroupAlliance ? _alliance.AllianceMember(i - PartyState.MaxPartySize) : null;
- UpdatePartySlot(i, 0, member != null ? member->ObjectID : 0);
+ var member = isNormalAlliance ? gm->AllianceMembers.GetPointer(i - PartyState.MaxPartySize) : null;
+ if (member != null && !member->IsValidAllianceMember)
+ member = null;
+ UpdatePartySlot(i, 0, member != null ? member->ObjectId : 0);
}
// update limit break
var lb = LimitBreakController.Instance();
- var lbMax = (ushort)lb->BarValue; // CS is incorrect here, two high bytes are some other fields
- if (_ws.Party.LimitBreakCur != lb->CurrentValue || _ws.Party.LimitBreakMax != lbMax)
- _ws.Execute(new PartyState.OpLimitBreakChange(lb->CurrentValue, lbMax));
+ if (_ws.Party.LimitBreakCur != lb->CurrentUnits || _ws.Party.LimitBreakMax != lb->BarUnits)
+ _ws.Execute(new PartyState.OpLimitBreakChange(lb->CurrentUnits, lb->BarUnits));
+ }
+
+ private unsafe bool HasBuddy(ulong instanceID)
+ {
+ var ui = UIState.Instance();
+ for (int i = 0; i < ui->Buddy.DutyHelperInfo.ENpcIds.Length; ++i)
+ if (ui->Buddy.DutyHelperInfo.DutyHelpers[i].ObjectId == instanceID)
+ return true;
+ return false;
+ }
+
+ private int FindFreePartySlot()
+ {
+ for (int i = 1; i < PartyState.MaxPartySize; ++i)
+ if (_ws.Party.ContentIDs[i] == 0 && _ws.Party.ActorIDs[i] == 0)
+ return i;
+ return -1;
+ }
+
+ private void AddPartyMember(ulong contentID, ulong instanceID)
+ {
+ var freeSlot = FindFreePartySlot();
+ if (freeSlot >= 0)
+ _ws.Execute(new PartyState.OpModify(freeSlot, contentID, instanceID));
+ else
+ Service.Log($"[WorldState] Failed to find empty slot for party member {contentID:X}:{instanceID:X}");
}
private void UpdatePartySlot(int slot, ulong contentID, ulong instanceID)
@@ -380,7 +443,8 @@ private void UpdatePartySlot(int slot, ulong contentID, ulong instanceID)
private unsafe void UpdateClient()
{
- var countdown = Countdown.TimeRemaining();
+ var countdownAgent = AgentCountDownSettingDialog.Instance();
+ float? countdown = countdownAgent != null && countdownAgent->Active && countdownAgent->TimeRemaining >= 0 ? countdownAgent->TimeRemaining : null;
if (_ws.Client.CountdownRemaining != countdown)
_ws.Execute(new ClientState.OpCountdownChange(countdown));
@@ -399,7 +463,12 @@ private unsafe void UpdateClient()
_ws.Execute(new ClientState.OpDutyActionsChange(dutyAction0, dutyAction1));
Span bozjaHolster = stackalloc byte[_ws.Client.BozjaHolster.Length];
- BozjaInterop.FetchHolster(bozjaHolster);
+ bozjaHolster.Clear();
+ var bozjaState = PublicContentBozja.GetState();
+ if (bozjaState != null)
+ foreach (var action in bozjaState->HolsterActions)
+ if (action != 0)
+ ++bozjaHolster[action];
if (!MemoryExtensions.SequenceEqual(_ws.Client.BozjaHolster.AsSpan(), bozjaHolster))
_ws.Execute(new ClientState.OpBozjaHolsterChange(CalcBozjaHolster(bozjaHolster)));
@@ -409,7 +478,7 @@ private unsafe void UpdateClient()
_ws.Execute(new ClientState.OpActiveFateChange(activeFate));
}
- private ulong SanitizedObjectID(ulong raw) => raw != GameObject.InvalidGameObjectId ? raw : 0;
+ private ulong SanitizedObjectID(ulong raw) => raw != InvalidEntityId ? raw : 0;
private void DispatchActorEvents(ulong instanceID)
{
@@ -474,6 +543,30 @@ private void OnEffectResult(ulong targetID, uint seq, int targetIndex)
_confirms.Add((seq, targetID, targetIndex));
}
+ private unsafe void ProcessPacketEffectResultDetour(uint targetID, byte* packet, byte replaying)
+ {
+ var count = packet[0];
+ var p = (Network.ServerIPC.EffectResultEntry*)(packet + 4);
+ for (int i = 0; i < count; ++i)
+ {
+ OnEffectResult(targetID, p->RelatedActionSequence, p->RelatedTargetIndex);
+ ++p;
+ }
+ _processPacketEffectResultHook.Original(targetID, packet, replaying);
+ }
+
+ private unsafe void ProcessPacketEffectResultBasicDetour(uint targetID, byte* packet, byte replaying)
+ {
+ var count = packet[0];
+ var p = (Network.ServerIPC.EffectResultBasicEntry*)(packet + 4);
+ for (int i = 0; i < count; ++i)
+ {
+ OnEffectResult(targetID, p->RelatedActionSequence, p->RelatedTargetIndex);
+ ++p;
+ }
+ _processPacketEffectResultBasicHook.Original(targetID, packet, replaying);
+ }
+
private void ProcessPacketActorControlDetour(uint actorID, uint category, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, ulong targetID, byte replaying)
{
_processPacketActorControlHook.Original(actorID, category, p1, p2, p3, p4, p5, p6, targetID, replaying);
diff --git a/BossMod/Network/PacketInterceptor.cs b/BossMod/Network/PacketInterceptor.cs
index 87aa346ce5..f1918a394d 100644
--- a/BossMod/Network/PacketInterceptor.cs
+++ b/BossMod/Network/PacketInterceptor.cs
@@ -1,5 +1,4 @@
-using Dalamud.Hooking;
-using System.Runtime.InteropServices;
+using System.Runtime.InteropServices;
namespace BossMod.Network;
@@ -25,19 +24,17 @@ internal sealed class PacketInterceptor : IDisposable
public event ServerIPCReceivedDelegate? ServerIPCReceived;
private unsafe delegate bool FetchReceivedPacketDelegate(void* self, ReceivedPacket* outData);
- private readonly Hook? _fetchHook;
+ private readonly HookAddress? _fetchHook;
public bool Active
{
- get => _fetchHook?.IsEnabled ?? false;
+ get => _fetchHook?.Enabled ?? false;
set
{
if (_fetchHook == null)
Service.Log($"[NPI] Hook not found!");
- else if (value)
- _fetchHook.Enable();
else
- _fetchHook.Disable();
+ _fetchHook.Enabled = value;
}
}
@@ -49,7 +46,7 @@ public unsafe PacketInterceptor()
var foundFetchAddress = Service.SigScanner.TryScanText("E8 ?? ?? ?? ?? 84 C0 0F 85 ?? ?? ?? ?? 48 8D 35", out var fetchAddress) || Service.SigScanner.TryScanText("E8 ?? ?? ?? ?? 84 C0 0F 85 ?? ?? ?? ?? 44 0F B6 64 24", out fetchAddress);
Service.Log($"[NPI] FetchReceivedPacket address = 0x{fetchAddress:X}");
if (foundFetchAddress)
- _fetchHook = Service.Hook.HookFromAddress(fetchAddress, FetchReceivedPacketDetour);
+ _fetchHook = new(fetchAddress, FetchReceivedPacketDetour, false);
// potentially useful sigs from dalamud:
// server ipc handler: 40 53 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 44 24 ?? 8B F2 --- void(void* self, uint targetId, void* dataPtr)
diff --git a/BossMod/Replay/Analysis/AbilityInfo.cs b/BossMod/Replay/Analysis/AbilityInfo.cs
index cc121cdfb4..07b3bcf90b 100644
--- a/BossMod/Replay/Analysis/AbilityInfo.cs
+++ b/BossMod/Replay/Analysis/AbilityInfo.cs
@@ -397,7 +397,7 @@ UITree.NodeProperties map(KeyValuePair kv)
{
var row = Service.LuminaRow(aid.ID);
tree.LeafNode($"Category: {row?.ActionCategory?.Value?.Name}");
- tree.LeafNode($"Cast time: {row?.Cast100ms * 0.1f:f1}");
+ tree.LeafNode($"Cast time: {row?.Cast100ms * 0.1f:f1} + {row?.Unknown38 * 0.1f:f1}");
tree.LeafNode($"Target range: {row?.Range}");
tree.LeafNode($"Effect shape: {row?.CastType} ({(row != null ? DescribeShape(row) : "")})");
tree.LeafNode($"Effect range: {row?.EffectRange}");
@@ -554,13 +554,14 @@ private IEnumerable ActionTargetStrings(ActionData data)
yield return OIDString(oid);
}
- private string CastTimeString(ActionData data) => data.CastTime > 0 ? string.Create(CultureInfo.InvariantCulture, $"{data.CastTime:f1}s cast") : "no cast";
+ private string CastTimeString(ActionData data, Lumina.Excel.GeneratedSheets.Action? ldata)
+ => data.CastTime > 0 ? string.Create(CultureInfo.InvariantCulture, $"{data.CastTime:f1}{(ldata?.Unknown38 > 0 ? $"+{ldata?.Unknown38 * 0.1f:f1}" : "")}s cast") : "no cast";
private string EnumMemberString(ActionID aid, ActionData data)
{
var ldata = aid.Type == ActionType.Spell ? Service.LuminaRow(aid.ID) : null;
string name = aid.Type != ActionType.Spell ? $"// {aid}" : _aidType?.GetEnumName(aid.ID) ?? $"_{Utils.StringToIdentifier(ldata?.ActionCategory?.Value?.Name ?? "")}_{Utils.StringToIdentifier(ldata?.Name ?? $"Ability{aid.ID}")}";
- return $"{name} = {aid.ID}, // {OIDListString(data.CasterOIDs)}->{JoinStrings(ActionTargetStrings(data))}, {CastTimeString(data)}, {DescribeShape(ldata)}";
+ return $"{name} = {aid.ID}, // {OIDListString(data.CasterOIDs)}->{JoinStrings(ActionTargetStrings(data))}, {CastTimeString(data, ldata)}, {DescribeShape(ldata)}";
}
private string DescribeShape(Lumina.Excel.GeneratedSheets.Action? data) => data != null ? data.CastType switch
diff --git a/BossMod/Replay/Visualization/EventList.cs b/BossMod/Replay/Visualization/EventList.cs
index 7538e19b13..9137ed4a72 100644
--- a/BossMod/Replay/Visualization/EventList.cs
+++ b/BossMod/Replay/Visualization/EventList.cs
@@ -1,4 +1,5 @@
using ImGuiNET;
+using System.Runtime.InteropServices;
namespace BossMod.ReplayVisualization;
@@ -31,13 +32,13 @@ public void Draw()
foreach (var e in _tree.Nodes(r.Encounters, e => new($"{ModuleRegistry.FindByOID(e.OID)?.ModuleType.Name}: {e.InstanceID:X}, zone={e.Zone}, start={e.Time.Start:O}, duration={e.Time}, countdown on pull={e.CountdownOnPull:f3}")))
{
var moduleInfo = ModuleRegistry.FindByOID(e.OID);
- var lists = _listsFiltered.GetOrAdd(e);
- foreach (var n in _tree.Node("Raw ops", contextMenu: () => OpListContextMenu(lists.Ops)))
+ ref var lists = ref CollectionsMarshal.GetValueRefOrAddDefault(_listsFiltered, e, out _);
+ foreach (var n in _tree.Node("Raw ops", contextMenu: () => OpListContextMenu(_listsFiltered[e].Ops)))
{
lists.Ops ??= new(r, moduleInfo, r.Ops.SkipWhile(o => o.Timestamp < e.Time.Start).TakeWhile(o => o.Timestamp <= e.Time.End), scrollTo);
lists.Ops.Draw(_tree, e.Time.Start);
}
- foreach (var n in _tree.Node("Server IPCs", contextMenu: () => IPCListContextMenu(lists.IPCs)))
+ foreach (var n in _tree.Node("Server IPCs", contextMenu: () => IPCListContextMenu(_listsFiltered[e].IPCs)))
{
lists.IPCs ??= new(r, r.Ops.SkipWhile(o => o.Timestamp < e.Time.Start).TakeWhile(o => o.Timestamp <= e.Time.End), scrollTo);
lists.IPCs.Draw(_tree, e.Time.Start);
diff --git a/BossMod/Timeline/ColumnPlannerTrackTarget.cs b/BossMod/Timeline/ColumnPlannerTrackTarget.cs
index 5c979c62c6..e70ba1ee4c 100644
--- a/BossMod/Timeline/ColumnPlannerTrackTarget.cs
+++ b/BossMod/Timeline/ColumnPlannerTrackTarget.cs
@@ -65,7 +65,7 @@ protected override void EditElement(Element e)
}
}
- private string OIDString(uint oid) => oid == 0 ? "Automatic" : $"{ModuleInfo?.ObjectIDType?.GetEnumName(oid)} (0x{oid})";
+ private string OIDString(uint oid) => oid == 0 ? "Automatic" : $"{ModuleInfo?.ObjectIDType?.GetEnumName(oid)} (0x{oid:X})";
private OverrideElement SetElementValue(OverrideElement e, uint oid)
{
diff --git a/BossMod/Util/FourCC.cs b/BossMod/Util/FourCC.cs
index 7ece3c8d0a..eb31a0a7e2 100644
--- a/BossMod/Util/FourCC.cs
+++ b/BossMod/Util/FourCC.cs
@@ -1,5 +1,4 @@
-using System.Runtime.CompilerServices;
-using System.Text;
+using System.Text;
namespace BossMod;
diff --git a/BossMod/Util/Hook.cs b/BossMod/Util/Hook.cs
new file mode 100644
index 0000000000..7f528f3ee4
--- /dev/null
+++ b/BossMod/Util/Hook.cs
@@ -0,0 +1,34 @@
+using Dalamud.Hooking;
+using InteropGenerator.Runtime;
+
+namespace BossMod;
+
+// very simple wrappers for hooks, that provide some quality of life (no need to repeat delegate types multiple times, etc)
+public sealed class HookAddress : IDisposable where T : Delegate
+{
+ private readonly Hook _hook;
+
+ public nint Address => _hook.Address;
+ public T Original => _hook.Original;
+ public bool Enabled
+ {
+ get => _hook.IsEnabled;
+ set
+ {
+ if (value)
+ _hook.Enable();
+ else
+ _hook.Disable();
+ }
+ }
+
+ public HookAddress(Address address, T detour, bool autoEnable = true) : this(address.Value, detour, autoEnable) { }
+ public HookAddress(nint address, T detour, bool autoEnable = true)
+ {
+ _hook = Service.Hook.HookFromAddress(address, detour);
+ if (autoEnable)
+ _hook.Enable();
+ }
+
+ public void Dispose() => _hook.Dispose();
+}
diff --git a/BossMod/packages.lock.json b/BossMod/packages.lock.json
index b3c808cf79..30cc957fc0 100644
--- a/BossMod/packages.lock.json
+++ b/BossMod/packages.lock.json
@@ -18,6 +18,11 @@
"SharpDX": "4.2.0"
}
},
+ "JetBrains.Annotations": {
+ "type": "Transitive",
+ "resolved": "2021.2.0",
+ "contentHash": "kKSyoVfndMriKHLfYGmr0uzQuI4jcc3TKGyww7buJFCYeHb/X0kodYBPL7n9454q7v6ASiRmDgpPGaDGerg/Hg=="
+ },
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "1.1.0",
@@ -923,6 +928,16 @@
"System.Threading": "4.3.0",
"System.Xml.ReaderWriter": "4.3.0"
}
+ },
+ "ffxivclientstructs": {
+ "type": "Project",
+ "dependencies": {
+ "InteropGenerator.Runtime": "[1.0.0, )",
+ "JetBrains.Annotations": "[2021.2.0, )"
+ }
+ },
+ "interopgenerator.runtime": {
+ "type": "Project"
}
}
}
diff --git a/FFXIVClientStructs b/FFXIVClientStructs
new file mode 160000
index 0000000000..44fb68a549
--- /dev/null
+++ b/FFXIVClientStructs
@@ -0,0 +1 @@
+Subproject commit 44fb68a549f6e2a4b2674a7e9687cd2054de5f36
diff --git a/TODO b/TODO
index 01b214aa76..9a26c9b344 100644
--- a/TODO
+++ b/TODO
@@ -1,3 +1,18 @@
+autorotation v2:
+- Autorotation.cs
+-- not tied to game - convert to rotationmodulemanager basically
+-- move useaction hook to AMEx
+-- manual queue should be managed by a plugin / framework
+-- move hints above? ie to be filled by framework right after WS/BMM update
+-- manage ui and active modules
+-- calcnextaction logic to combine manual queue, hints and rotation modules
+- CommonActions.cs
+-- just a base class for rotation modules
+-- move rotation expire logic to autorotation?
+--- maybe need to split into manager + manual layers?
+--
+
+
autorotation rework:
- priority action suggestions
- control panel w/ presets and bindings
diff --git a/cs_crosscheck b/cs_crosscheck
new file mode 100644
index 0000000000..0f8061c59e
--- /dev/null
+++ b/cs_crosscheck
@@ -0,0 +1,11 @@
+public static unsafe byte GameObjectEventState(GameObject obj) => ReadField(GameObjectInternal(obj), 0x70); // see actor control 106
+public static unsafe byte CharacterShieldValue(Character chr) => ReadField(CharacterInternal(chr), 0x1A0 + 0x46); // CharacterInternal(chr)->ShieldValue; // % of max hp; see effect result
+public static unsafe bool CharacterInCombat(Character chr) => (ReadField(CharacterInternal(chr), 0x1EB) & 0x20) != 0; // see actor control 4
+public static unsafe byte CharacterAnimationState(Character chr, bool second) => ReadField(CharacterInternal(chr), 0x970 + (second ? 0x2C2 : 0x2C1)); // see actor control 62
+public static unsafe byte CharacterModelState(Character chr) => ReadField(CharacterInternal(chr), 0x970 + 0x2C0); // see actor control 63
+public static unsafe float CharacterCastRotation(Character chr) => ReadField(CharacterInternal(chr), 0x1B6C); // see ActorCast -> Character::StartCast
+public static unsafe ulong CharacterTargetID(Character chr) => ReadField(CharacterInternal(chr), 0x1B58); // until FFXIVClientStructs fixes offset and type...
+public static unsafe ushort CharacterTetherID(Character chr) => ReadField(CharacterInternal(chr), 0x12F0 + 0xA0); // see actor control 35 -> CharacterTethers::Set (note that there is also a secondary tether...)
+public static unsafe ulong CharacterTetherTargetID(Character chr) => ReadField(CharacterInternal(chr), 0x12F0 + 0xA0 + 0x10);
+public static unsafe Vector3 BattleCharaCastLocation(BattleChara chara) => BattleCharaInternal(chara)->GetCastInfo()->CastLocation; // see ActorCast -> Character::StartCast -> Character::StartOmen
+