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 +