diff --git a/BossMod/AI/AIBehaviour.cs b/BossMod/AI/AIBehaviour.cs index fd6c99b49a..5580f05dfc 100644 --- a/BossMod/AI/AIBehaviour.cs +++ b/BossMod/AI/AIBehaviour.cs @@ -15,11 +15,6 @@ sealed class AIBehaviour(AIController ctrl, Autorotation autorot) : IDisposable private WPos _masterMovementStart; private DateTime _masterLastMoved; - public bool ForbidActions => _config.ForbidActions; - public bool ForbidMovement => _config.ForbidMovement; - public bool FollowDuringCombat => _config.FollowDuringCombat; - public bool FollowDuringActiveBossModule => _config.FollowDuringActiveBossModule; - public void Dispose() { } @@ -34,7 +29,7 @@ public void Execute(Actor player, Actor master) FocusMaster(master); _afkMode = !master.InCombat && (autorot.WorldState.CurrentTime - _masterLastMoved).TotalSeconds > 10; - bool forbidActions = _config.ForbidActions || ctrl.IsMounted || _afkMode || autorot.ClassActions == null || autorot.ClassActions.AutoAction >= CommonActions.AutoActionFirstCustom; + var forbidActions = _config.ForbidActions || ctrl.IsMounted || _afkMode || autorot.ClassActions == null || autorot.ClassActions.AutoAction >= CommonActions.AutoActionFirstCustom; CommonActions.Targeting target = new(); if (!forbidActions) @@ -46,14 +41,14 @@ public void Execute(Actor player, Actor master) AdjustTargetPositional(player, ref target); } - _followMaster = master != player && (_config.FollowDuringCombat || !master.InCombat || (_masterPrevPos - _masterMovementStart).LengthSq() > 100) && (_config.FollowDuringActiveBossModule || autorot.Bossmods.ActiveModule?.StateMachine.ActiveState == null); - _naviDecision = BuildNavigationDecision(player, master, ref target); + var followTarget = _config.FollowTarget; + _followMaster = master != player && (_config.FollowDuringCombat || !master.InCombat || (_masterPrevPos - _masterMovementStart).LengthSq() > 100) && (_config.FollowDuringActiveBossModule || autorot.Bossmods.ActiveModule?.StateMachine.ActiveState == null) && (_config.FollowOutOfCombat || master.InCombat); - bool masterIsMoving = TrackMasterMovement(master); - bool moveWithMaster = masterIsMoving && (master == player || _followMaster); + _naviDecision = followTarget && autorot.WorldState.Actors.Find(player.TargetID) != null ? BuildNavigationDecision(player, autorot.WorldState.Actors.Find(player.TargetID)!, ref target) : BuildNavigationDecision(player, master, ref target); + var masterIsMoving = TrackMasterMovement(master); + var moveWithMaster = masterIsMoving && (master == player || _followMaster); _maxCastTime = moveWithMaster || ctrl.ForceFacing ? 0 : _naviDecision.LeewaySeconds; - // note: that there is a 1-frame delay if target and/or strategy changes - we don't really care?.. if (!forbidActions) { int actionStrategy = target.Target != null ? CommonActions.AutoActionAIFight : CommonActions.AutoActionAIIdle; @@ -104,13 +99,15 @@ private void AdjustTargetPositional(Actor player, ref CommonActions.Targeting ta private NavigationDecision BuildNavigationDecision(Actor player, Actor master, ref CommonActions.Targeting targeting) { - if (_config.ForbidActions) + var target = autorot.WorldState.Actors.Find(player.TargetID); + if (_config.ForbidMovement) return new() { LeewaySeconds = float.MaxValue }; - if (_followMaster) + if (_followMaster && !_config.FollowTarget || _followMaster && _config.FollowTarget && target == null) return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, master.Position, 1, new(), Positional.Any); + if (_followMaster && _config.FollowTarget && target != null) + return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, target.Position, target.HitboxRadius + player.HitboxRadius + 3, target.Rotation, _config.DesiredPositional); if (targeting.Target == null) return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, null, 0, new(), Positional.Any); - var adjRange = targeting.PreferredRange + player.HitboxRadius + targeting.Target.Actor.HitboxRadius; if (targeting.PreferTanking) { @@ -123,8 +120,8 @@ private NavigationDecision BuildNavigationDecision(Actor player, Actor master, r return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, dest, 0.5f, new(), Positional.Any); } } - var adjRotation = targeting.PreferTanking ? targeting.Target.DesiredRotation : targeting.Target.Actor.Rotation; + return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, targeting.Target.Actor.Position, adjRange, adjRotation, targeting.PreferredPosition); } @@ -206,8 +203,12 @@ public void DrawDebug() ImGui.Checkbox("Forbid movement", ref _config.ForbidMovement); ImGui.SameLine(); ImGui.Checkbox("Follow during combat", ref _config.FollowDuringCombat); - ImGui.SameLine(); + ImGui.Spacing(); ImGui.Checkbox("Follow during active boss module", ref _config.FollowDuringActiveBossModule); + ImGui.SameLine(); + ImGui.Checkbox("Follow out of combat", ref _config.FollowOutOfCombat); + ImGui.SameLine(); + ImGui.Checkbox("Follow target", ref _config.FollowTarget); var player = autorot.WorldState.Party.Player(); var dist = _naviDecision.Destination != null && player != null ? (_naviDecision.Destination.Value - player.Position).Length() : 0; ImGui.TextUnformatted($"Max-cast={MathF.Min(_maxCastTime, 1000):f3}, afk={_afkMode}, follow={_followMaster}, algo={_naviDecision.DecisionType} {_naviDecision.Destination} (d={dist:f3}), master standing for {Math.Clamp((autorot.WorldState.CurrentTime - _masterLastMoved).TotalSeconds, 0, 1000):f1}"); diff --git a/BossMod/AI/AIConfig.cs b/BossMod/AI/AIConfig.cs index cd314e2f12..1fe1716b59 100644 --- a/BossMod/AI/AIConfig.cs +++ b/BossMod/AI/AIConfig.cs @@ -6,16 +6,19 @@ sealed class AIConfig : ConfigNode [PropertyDisplay("Enable AI")] public bool Enabled = false; + [PropertyDisplay("Show status in DTR bar")] + public bool ShowDTR = false; + [PropertyDisplay("Draw UI")] public bool DrawUI = true; - [PropertyDisplay("Focus Target Leader")] + [PropertyDisplay("Focus target leader")] public bool FocusTargetLeader = true; [PropertyDisplay("Broadcast keypresses to other windows")] public bool BroadcastToSlaves = false; - [PropertyDisplay("Follow Party Slot")] + [PropertyDisplay("Follow party slot")] [PropertyCombo(["Slot 1", "Slot 2", "Slot 3", "Slot 4", "Slot 5", "Slot 6", "Slot 7", "Slot 8"])] public int FollowSlot = 0; @@ -30,4 +33,14 @@ sealed class AIConfig : ConfigNode [PropertyDisplay("Follow during active boss module")] public bool FollowDuringActiveBossModule = false; + + [PropertyDisplay("Follow out of combat")] + public bool FollowOutOfCombat = true; + + [PropertyDisplay("Follow target")] + public bool FollowTarget = false; + + [PropertyDisplay("Desired positional when following target")] + [PropertyCombo(["Any", "Flank", "Rear", "Front"])] + public Positional DesiredPositional = Positional.Any; } diff --git a/BossMod/AI/AIManager.cs b/BossMod/AI/AIManager.cs index 207bd19023..bb15283bb9 100644 --- a/BossMod/AI/AIManager.cs +++ b/BossMod/AI/AIManager.cs @@ -1,4 +1,5 @@ -using Dalamud.Game.Text; +using Dalamud.Game.Gui.Dtr; +using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using ImGuiNET; @@ -10,6 +11,7 @@ sealed class AIManager : IDisposable private readonly Autorotation _autorot; private readonly AIController _controller; private readonly AIConfig _config; + private readonly DtrBarEntry _dtrBarEntry; private int _masterSlot = PartyState.PlayerSlot; // non-zero means corresponding player is master private AIBehaviour? _beh; private readonly UISimpleWindow _ui; @@ -20,6 +22,7 @@ public AIManager(Autorotation autorot) _controller = new(); _config = Service.Config.Get(); _ui = new("AI", DrawOverlay, false, new(100, 100), ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoFocusOnAppearing) { RespectCloseHotkey = false }; + _dtrBarEntry = Service.DtrBar.Get("Bossmod"); Service.ChatGui.ChatMessage += OnChatMessage; Service.CommandManager.AddHandler("/bmrai", new Dalamud.Game.Command.CommandInfo(OnCommand) { HelpMessage = "Toggle AI mode" }); Service.CommandManager.AddHandler("/vbmai", new Dalamud.Game.Command.CommandInfo(OnCommand) { ShowInHelp = false }); @@ -29,6 +32,7 @@ public void Dispose() { SwitchToIdle(); _ui.Dispose(); + _dtrBarEntry.Dispose(); Service.ChatGui.ChatMessage -= OnChatMessage; Service.CommandManager.RemoveHandler("/bmrai"); Service.CommandManager.RemoveHandler("/vbmai"); @@ -55,6 +59,25 @@ public void Update() _controller.Update(player); _ui.IsOpen = _config.Enabled && player != null && _config.DrawUI; + + DtrUpdate(_beh); + } + + public void DtrUpdate(AIBehaviour? behaviour) + { + _dtrBarEntry.Shown = _config.ShowDTR; + if (_dtrBarEntry.Shown) + { + var status = behaviour != null ? "On" : "Off"; + _dtrBarEntry.Text = "AI: " + status; + _dtrBarEntry.OnClick = () => + { + if (behaviour != null) + SwitchToIdle(); + else + SwitchToFollow(_config.FollowSlot); + }; + } } private void DrawOverlay() @@ -62,12 +85,12 @@ private void DrawOverlay() ImGui.TextUnformatted($"AI: {(_beh != null ? "on" : "off")}, master={_autorot.WorldState.Party[_masterSlot]?.Name}"); ImGui.TextUnformatted($"Navi={_controller.NaviTargetPos} / {_controller.NaviTargetRot}{(_controller.ForceFacing ? " forced" : "")}"); _beh?.DrawDebug(); - if (ImGui.Button("Reset")) - SwitchToIdle(); - ImGui.SameLine(); - if (ImGui.Button("AI On - Follow selected slot")) + if (ImGui.Button("AI on")) SwitchToFollow(_config.FollowSlot); - ImGui.Text("Follow Party Slot"); + ImGui.SameLine(); + if (ImGui.Button("AI off")) + SwitchToIdle(); + ImGui.Text("Follow party slot"); ImGui.SameLine(); var partyMemberNames = new List(); for (var i = 0; i < 8; i++) @@ -81,6 +104,13 @@ private void DrawOverlay() var partyMemberNamesArray = partyMemberNames.ToArray(); ImGui.Combo("##FollowPartySlot", ref _config.FollowSlot, partyMemberNamesArray, partyMemberNamesArray.Length); + + ImGui.Text("Desired positional"); + ImGui.SameLine(); + var positionalOptions = Enum.GetNames(typeof(Positional)); + var positionalIndex = (int)_config.DesiredPositional; + if (ImGui.Combo("##DesiredPositional", ref positionalIndex, positionalOptions, positionalOptions.Length)) + _config.DesiredPositional = (Positional)positionalIndex; } private void SwitchToIdle() @@ -113,10 +143,7 @@ private int FindPartyMemberSlotFromSender(SeString sender) // Check for NPCs (Buddies) var buddy = _autorot.WorldState.Party.WithSlot().FirstOrDefault(p => p.Item2.Name.Equals(source.PlayerName, StringComparison.OrdinalIgnoreCase)); - if (buddy != default) - return buddy.Item1; - - return -1; + return buddy != default ? buddy.Item1 : -1; } private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) @@ -203,6 +230,10 @@ private void OnCommand(string cmd, string message) _config.ForbidMovement = !_config.ForbidMovement; Service.Log($"[AI] Forbid movement is now {(_config.ForbidMovement ? "enabled" : "disabled")}"); break; + case "followoutofcombat": + _config.FollowOutOfCombat = !_config.FollowOutOfCombat; + Service.Log($"[AI] Forbid movement is now {(_config.FollowOutOfCombat ? "enabled" : "disabled")}"); + break; case "followcombat": if (_config.FollowDuringCombat) { @@ -229,12 +260,47 @@ private void OnCommand(string cmd, string message) Service.Log($"[AI] Follow during active boss module is now {(_config.FollowDuringActiveBossModule ? "enabled" : "disabled")}"); Service.Log($"[AI] Follow during combat is now {(_config.FollowDuringCombat ? "enabled" : "disabled")}"); break; + case "followtarget": + _config.FollowTarget = !_config.FollowTarget; + Service.Log($"[AI] Following targets is now {(_config.FollowTarget ? "enabled" : "disabled")}"); + break; + case "positional": + if (messageData.Length < 2) + { + Service.Log("[AI] Missing positional type."); + return; + } + SetPositional(messageData[1]); + break; default: Service.Log($"[AI] Unknown command: {messageData[0]}"); break; } } + private void SetPositional(string positional) + { + switch (positional.ToLower()) + { + case "any": + _config.DesiredPositional = Positional.Any; + break; + case "flank": + _config.DesiredPositional = Positional.Flank; + break; + case "rear": + _config.DesiredPositional = Positional.Rear; + break; + case "front": + _config.DesiredPositional = Positional.Front; + break; + default: + Service.Log($"[AI] Unknown positional: {positional}"); + return; + } + Service.Log($"[AI] Desired positional set to {_config.DesiredPositional}"); + } + private int FindPartyMemberByName(string name) { for (var i = 0; i < 8; i++) diff --git a/BossMod/Framework/ActionManagerConfig.cs b/BossMod/ActionTweaks/ActionManagerConfig.cs similarity index 94% rename from BossMod/Framework/ActionManagerConfig.cs rename to BossMod/ActionTweaks/ActionManagerConfig.cs index 68d6b76751..4239a33630 100644 --- a/BossMod/Framework/ActionManagerConfig.cs +++ b/BossMod/ActionTweaks/ActionManagerConfig.cs @@ -1,7 +1,8 @@ namespace BossMod; +// TODO: rename [ConfigDisplay(Name = "Action tweaks settings", Order = 4)] -class ActionManagerConfig : ConfigNode +public sealed class ActionManagerConfig : ConfigNode { // TODO: consider exposing max-delay to config; 0 would mean 'remove all delay', max-value would mean 'disable' [PropertyDisplay("Remove extra lag-induced animation lock delay from instant casts (a-la xivalex)")] diff --git a/BossMod/ActionTweaks/AnimationLockTweak.cs b/BossMod/ActionTweaks/AnimationLockTweak.cs new file mode 100644 index 0000000000..3fa87e7c5f --- /dev/null +++ b/BossMod/ActionTweaks/AnimationLockTweak.cs @@ -0,0 +1,43 @@ +namespace BossMod; + +// Effective animation lock reduction tweak (a-la xivalex/noclippy). +// The game handles instants and casted actions differently: +// * instants: on action request (e.g. on the frame the action button is pressed), animation lock is set to 0.5 (or 0.35 for some specific actions); it then ticks down every frame +// some time later (ping + server latency, typically 50-100ms if ping is good), we receive action effect packet - the packet contains action's animation lock (typically 0.6) +// the game then updates animation lock (now equal to 0.5 minus time since request) to the packet data +// so the 'effective' animation lock between action request and animation lock end is equal to action's animation lock + delay between request and response +// this tweak reduces effective animation lock by either removing extra delay completely or clamping it to specified min/max values +// * casts: on action request animation lock is not set (remains equal to 0), remaining cast time is set to action's cast time; remaining cast time then ticks down every frame +// some time later (cast time minus approximately 0.5s, aka slidecast window), we receive action effect packet - the packet contains action's animation lock (typically 0.1) +// the game then updates animation lock (still 0) to the packet data - however, since animation lock isn't ticking down while cast is in progress, there is no extra delay +// this tweak does nothing for casts, since they already work correctly +// The tweak also allows predicting the delay based on history (using exponential average). +// TODO: consider adding 'clamped delay' mode that doesn't reduce it straight to zero (a-la xivalex)? +public sealed class AnimationLockTweak +{ + private readonly ActionManagerConfig _config = Service.Config.Get(); + + public float DelaySmoothing = 0.8f; // TODO tweak + public float DelayAverage { get; private set; } = 0.1f; // smoothed delay between client request and server response + public float DelayEstimate => _config.RemoveAnimationLockDelay ? 0 : MathF.Min(DelayAverage * 1.5f, 0.1f); // this is a conservative estimate + + // perform sanity check to detect conflicting plugins: disable the tweak if condition is false + public void SanityCheck(float originalAnimLock, float modifiedAnimLock) + { + if (!_config.RemoveAnimationLockDelay) + return; // nothing to do, tweak is already disabled + if (originalAnimLock == modifiedAnimLock && originalAnimLock % 0.01 is <= 0.0005f or >= 0.0095f) + return; // nothing changed the packet value, and it's original value is reasonable + + Service.Log($"[ALT] Unexpected animation lock {originalAnimLock:f} -> {modifiedAnimLock:f}, disabling anim lock tweak feature"); + _config.RemoveAnimationLockDelay = false; // disable the tweak (but don't save the config, in case this condition is temporary) + } + + // apply tweak: given the delay, calculate how much it should be reduced + public float Apply(float current, float delay) + { + DelayAverage = delay * (1 - DelaySmoothing) + DelayAverage * DelaySmoothing; // update the average + // the result will be subtracted from current anim lock (and thus from adjusted lock delay) + return _config.RemoveCooldownDelay ? Math.Clamp(delay /* - DelayMax */, 0, current) : 0; + } +} diff --git a/BossMod/Autorotation/Autorotation.cs b/BossMod/Autorotation/Autorotation.cs index b72d725660..112e1b6c0a 100644 --- a/BossMod/Autorotation/Autorotation.cs +++ b/BossMod/Autorotation/Autorotation.cs @@ -42,7 +42,7 @@ sealed class Autorotation : IDisposable public Actor? SecondaryTarget; // this is usually a mouseover, but AI can override; typically used for heal and utility abilities public AIHints Hints = new(); public float EffAnimLock => ActionManagerEx.Instance!.EffectiveAnimationLock; - public float AnimLockDelay => ActionManagerEx.Instance!.EffectiveAnimationLockDelay; + public float AnimLockDelay => ActionManagerEx.Instance!.AnimLockTweak.DelayEstimate; private static readonly ActionID IDSprintGeneral = new(ActionType.General, 4); @@ -96,7 +96,7 @@ public unsafe void Update() if (Hints.ForcedTarget != null && PrimaryTarget != Hints.ForcedTarget) { PrimaryTarget = Hints.ForcedTarget; - var obj = Hints.ForcedTarget.SpawnIndex >= 0 ? FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager.Instance()->Objects.All[Hints.ForcedTarget.SpawnIndex].Value : null; + var obj = Hints.ForcedTarget.SpawnIndex >= 0 ? FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager.Instance()->Objects.IndexSorted[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; diff --git a/BossMod/Autorotation/CommonActions.cs b/BossMod/Autorotation/CommonActions.cs index 29fbcf1113..e1f7f4cf92 100644 --- a/BossMod/Autorotation/CommonActions.cs +++ b/BossMod/Autorotation/CommonActions.cs @@ -328,7 +328,7 @@ protected void FillCommonPlayerState(CommonRotation.PlayerState s) s.TargetingEnemy = Autorot.PrimaryTarget != null && Autorot.PrimaryTarget.Type is ActorType.Enemy or ActorType.Part && !Autorot.PrimaryTarget.IsAlly; s.RangeToTarget = Autorot.PrimaryTarget != null ? (Autorot.PrimaryTarget.Position - Player.Position).Length() - Autorot.PrimaryTarget.HitboxRadius - Player.HitboxRadius : float.MaxValue; s.AnimationLock = am.EffectiveAnimationLock; - s.AnimationLockDelay = am.EffectiveAnimationLockDelay; + s.AnimationLockDelay = am.AnimLockTweak.DelayEstimate; s.ComboTimeLeft = am.ComboTimeLeft; s.ComboLastAction = am.ComboLastMove; s.LimitBreakLevel = Autorot.WorldState.Party.LimitBreakMax > 0 ? Autorot.WorldState.Party.LimitBreakCur / Autorot.WorldState.Party.LimitBreakMax : 0; diff --git a/BossMod/Autorotation/GNB/GNBRotation.cs b/BossMod/Autorotation/GNB/GNBRotation.cs index bcbe7c754f..8b9d3542cd 100644 --- a/BossMod/Autorotation/GNB/GNBRotation.cs +++ b/BossMod/Autorotation/GNB/GNBRotation.cs @@ -1,4 +1,4 @@ -// CONTRIB: made by lazylemo, not checked +// CONTRIB: made by LazyLemo, tweaked by Akechi (there's still plenty of issues that need to be addressed.. but with DT around the corner, not so much on my mind) namespace BossMod.GNB; public static class Rotation @@ -6,7 +6,7 @@ public static class Rotation // full state needed for determining next action public class State(WorldState ws) : CommonRotation.PlayerState(ws) { - public int Ammo; // 0 to 100 + public int Ammo; // 0 to 3 public int GunComboStep; // 0 to 2 public float NoMercyLeft; // 0 if buff not up, max 20 public bool ReadyToRip; // 0 if buff not up, max 10 @@ -34,31 +34,44 @@ public override string ToString() } // strategy configuration + // TODO: add in "Hold Double Down" & rotation to support it, I'm lazy public class Strategy : CommonRotation.Strategy { public enum GaugeUse : uint { - Automatic = 0, // spend gauge either under raid buffs or if next downtime is soon (so that next raid buff window won't cover at least 4 GCDs) + Automatic = 0, // optimal spend (for the most part) [PropertyDisplay("Spend all gauge ASAP", 0x8000ff00)] - Spend = 1, // spend all gauge asap, don't bother conserving + Spend = 1, // burn all carts; Double Down > GF combo > Burst Strike > 123 combo + + [PropertyDisplay("Hold Carts", 0x800000ff)] + Hold = 2, // Hold cartridges optimally; works for both ST/AOE + + [PropertyDisplay("Force ST combo", 0x809061F9)] + ForceST = 3, // forces Single Target combo & protects overcap + + [PropertyDisplay("Force AOE combo", 0x80D1AF97)] + ForceAOE = 4, // forces AOE combo & protects overcap + + [PropertyDisplay("Force Gnashing combo", 0x804C967D)] + ForceGF = 5, // forces GF combo [PropertyDisplay("Use Lightning Shot if outside melee", 0x80c08000)] - LightningShotIfNotInMelee = 2, + LightningShotIfNotInMelee = 6, [PropertyDisplay("Use ST combo if still in ST combo, else use AOE combo", 0x80c0c000)] - ComboFitBeforeDowntime = 3, // useful on late phases before downtime + ComboFitBeforeDowntime = 7, // useful on late phases before downtime [PropertyDisplay("Use appropriate rotation to reach max gauge before downtime (NEEDS TESTING)", 0x80c0c000)] - MaxGaugeBeforeDowntime = 4, // useful on late phases before downtime + MaxGaugeBeforeDowntime = 8, // useful on late phases before downtime [PropertyDisplay("Use combo until second-last step, then spend gauge", 0x80400080)] - PenultimateComboThenSpend = 5, // useful for ensuring ST extension is used right before long downtime + PenultimateComboThenSpend = 9, // useful for ensuring ST extension is used right before long downtime } public enum PotionUse : uint { - Manual = 0, // potion won't be used automatically + Manual = 0, // nothing used [PropertyDisplay("Use ASAP, but delay slightly during opener", 0x8000ff00)] Immediate = 1, @@ -245,10 +258,132 @@ public static AID GetNextAmmoAction(State state, Strategy strategy, bool aoe) { if (strategy.GaugeStrategy == Strategy.GaugeUse.Spend) { - if (state.CD(CDGroup.GnashingFang) > 9 && state.Ammo >= 1) - return AID.BurstStrike; - if (strategy.FightEndIn < 9 && state.Ammo > 0) + if (state.Ammo >= 2) + { + if (state.CD(CDGroup.DoubleDown) < 0.6f && state.Ammo > 2) + return AID.DoubleDown; + + if (state.CD(CDGroup.GnashingFang) < 0.6f && state.Ammo <= 2) + { + if (state.GunComboStep == 0) + return AID.GnashingFang; + if (state.GunComboStep == 1) + return AID.SavageClaw; + if (state.GunComboStep == 2) + return AID.WickedTalon; + } + return AID.BurstStrike; + } + + if (state.Ammo == 0 && state.GunComboStep <= 0) + { + return AID.KeenEdge; + } + } + + if (strategy.GaugeStrategy == Strategy.GaugeUse.Hold && state.Ammo >= 0 && state.NoMercyLeft >= 0) + { + if (aoe) + { + if (state.ComboLastMove == AID.DemonSlice && state.Ammo < 3) + { + return AID.DemonSlaughter; + } + else if (state.ComboLastMove == AID.DemonSlice && state.Ammo == 3) + { + return AID.FatedCircle; + } + else if (state.ComboLastMove == AID.KeenEdge) + { + return AID.BrutalShell; + } + + return AID.DemonSlice; + } + else if (state.ComboLastMove == AID.BrutalShell) + { + if (state.Ammo < 3) + { + return AID.SolidBarrel; + } + else if (state.Ammo == 3) + { + return AID.BurstStrike; + } + } + else if (state.ComboLastMove == AID.KeenEdge) + { + return AID.BrutalShell; + } + return AID.KeenEdge; + } + + if (strategy.GaugeStrategy == Strategy.GaugeUse.ForceST && state.Ammo >= 0 && state.NoMercyLeft >= 0) + { + if (state.ComboLastMove == AID.BrutalShell) + { + if (state.Ammo < 3) + { + return AID.SolidBarrel; + } + else if (state.Ammo == 3) + { + return AID.BurstStrike; + } + } + else if (state.ComboLastMove == AID.KeenEdge) + { + return AID.BrutalShell; + } + return AID.KeenEdge; + } + + if (strategy.GaugeStrategy == Strategy.GaugeUse.ForceAOE && state.Ammo >= 0 && state.NoMercyLeft >= 0) + { + if (strategy.GaugeStrategy == Strategy.GaugeUse.ForceAOE && state.Ammo >= 0 && state.NoMercyLeft >= 0) + { + if (state.ComboLastMove == AID.DemonSlice && state.Ammo < 3) + { + return AID.DemonSlaughter; + } + else if (state.ComboLastMove == AID.DemonSlice && state.Ammo == 3) + { + return AID.FatedCircle; + } + + return AID.DemonSlice; + } + } + + if (strategy.GaugeStrategy == Strategy.GaugeUse.ForceGF) + { + if (state.Ammo >= 1) + { + if (state.CD(CDGroup.GnashingFang) < 0.6f && state.CD(CDGroup.DoubleDown) < 0.6f) + { + if (state.GunComboStep == 0) + { + return AID.GnashingFang; + } + else if (state.GunComboStep == 1) + { + return AID.SavageClaw; + } + else if (state.GunComboStep == 2) + { + return AID.WickedTalon; + } + } + else if (state.GunComboStep == 1) + { + return AID.SavageClaw; + } + else if (state.GunComboStep == 2) + { + return AID.WickedTalon; + } + } } if (Service.Config.Get().Skscheck && state.Ammo == state.MaxCartridges - 1 && state.ComboLastMove == AID.BrutalShell && state.GunComboStep == 0 && state.CD(CDGroup.GnashingFang) < 2.5) @@ -281,6 +416,11 @@ public static AID GetNextAmmoAction(State state, Strategy strategy, bool aoe) return AID.BurstStrike; if (!aoe && state.Ammo >= 1 && state.CD(CDGroup.GnashingFang) > state.GCD && !state.Unlocked(AID.DoubleDown) && !state.Unlocked(AID.SonicBreak) && state.GunComboStep == 0) return AID.BurstStrike; + + // Lv70 only; when in NM and you can't use Fated Circle (Lv72) sadge + if (aoe && state.Ammo >= 1 && !state.Unlocked(AID.FatedCircle) && !state.Unlocked(AID.DoubleDown) && !state.Unlocked(AID.Bloodfest) && state.Unlocked(AID.Continuation) && state.GunComboStep == 0) + return AID.BurstStrike; + if (!aoe && state.Ammo >= 1 && !state.Unlocked(AID.DoubleDown) && !state.Unlocked(AID.SonicBreak) && !state.Unlocked(AID.GnashingFang)) return AID.BurstStrike; if (aoe && state.Ammo >= 1 && state.CD(CDGroup.GnashingFang) > state.GCD && state.CD(CDGroup.DoubleDown) > state.GCD && state.CD(CDGroup.SonicBreak) > state.GCD && state.Unlocked(AID.DoubleDown) && state.GunComboStep == 0) @@ -313,9 +453,129 @@ public static AID GetNextAmmoAction(State state, Strategy strategy, bool aoe) if (strategy.GaugeStrategy == Strategy.GaugeUse.Spend && state.Ammo >= 0) { - if (state.CD(CDGroup.GnashingFang) < 0.6f) - return AID.GnashingFang; - return AID.BurstStrike; + if (state.Ammo >= 2) + { + if (state.CD(CDGroup.DoubleDown) < 0.6f && state.Ammo > 2) + return AID.DoubleDown; + + if (state.CD(CDGroup.GnashingFang) < 0.6f && state.Ammo <= 2) + { + if (state.GunComboStep == 0) + return AID.GnashingFang; + if (state.GunComboStep == 1) + return AID.SavageClaw; + if (state.GunComboStep == 2) + return AID.WickedTalon; + } + + return AID.BurstStrike; + } + + if (state.Ammo == 0 && state.GunComboStep <= 0) + { + return AID.KeenEdge; + } + } + + if (strategy.GaugeStrategy == Strategy.GaugeUse.Hold && state.Ammo >= 0 && state.NoMercyLeft >= 0) + { + if (aoe) + { + if (state.ComboLastMove == AID.DemonSlice) + { + return AID.DemonSlaughter; + } + else if (state.ComboLastMove == AID.BrutalShell) + { + return AID.SolidBarrel; + } + else if (state.ComboLastMove == AID.KeenEdge) + { + return AID.BrutalShell; + } + + return AID.DemonSlice; + } + else if (state.ComboLastMove == AID.BrutalShell) + { + if (state.Ammo < 3) + { + return AID.SolidBarrel; + } + else if (state.Ammo == 3) + { + return AID.BurstStrike; + } + } + else if (state.ComboLastMove == AID.KeenEdge) + { + return AID.BrutalShell; + } + return AID.KeenEdge; + } + + if (strategy.GaugeStrategy == Strategy.GaugeUse.ForceST && state.Ammo >= 0 && state.NoMercyLeft >= 0) + { + if (state.ComboLastMove == AID.BrutalShell) + { + if (state.Ammo < 3) + { + return AID.SolidBarrel; + } + else if (state.Ammo == 3) + { + return AID.BurstStrike; + } + } + else if (state.ComboLastMove == AID.KeenEdge) + { + return AID.BrutalShell; + } + return AID.KeenEdge; + } + + if (strategy.GaugeStrategy == Strategy.GaugeUse.ForceAOE && state.Ammo >= 0 && state.NoMercyLeft >= 0) + { + if (state.ComboLastMove == AID.DemonSlice && state.Ammo < 3) + { + return AID.DemonSlaughter; + } + else if (state.ComboLastMove == AID.DemonSlice && state.Ammo == 3) + { + return AID.FatedCircle; + } + + return AID.DemonSlice; + } + + if (strategy.GaugeStrategy == Strategy.GaugeUse.ForceGF) + { + if (state.Ammo >= 1) + { + if (state.CD(CDGroup.GnashingFang) < 0.6f && state.CD(CDGroup.DoubleDown) < 0.6f) + { + if (state.GunComboStep == 0) + { + return AID.GnashingFang; + } + else if (state.GunComboStep == 1) + { + return AID.SavageClaw; + } + else if (state.GunComboStep == 2) + { + return AID.WickedTalon; + } + } + else if (state.GunComboStep == 1) + { + return AID.SavageClaw; + } + else if (state.GunComboStep == 2) + { + return AID.WickedTalon; + } + } } // single-target gauge spender @@ -326,6 +586,10 @@ public static AID GetNextAmmoAction(State state, Strategy strategy, bool aoe) { Strategy.GaugeUse.Automatic or Strategy.GaugeUse.LightningShotIfNotInMelee => (state.RaidBuffsLeft > state.GCD || strategy.FightEndIn <= strategy.RaidBuffsIn + 10), Strategy.GaugeUse.Spend => true, + Strategy.GaugeUse.ForceST => true, + Strategy.GaugeUse.ForceAOE => true, + Strategy.GaugeUse.ForceGF => true, + Strategy.GaugeUse.Hold => true, Strategy.GaugeUse.ComboFitBeforeDowntime => strategy.FightEndIn <= state.GCD + 2.5f * ((aoe ? GetAOEComboLength(state.ComboLastMove) : GetSTComboLength(state.ComboLastMove)) - 1), Strategy.GaugeUse.PenultimateComboThenSpend => state.ComboLastMove is AID.BrutalShell or AID.DemonSlice, _ => true @@ -342,11 +606,11 @@ public static AID GetNextAmmoAction(State state, Strategy strategy, bool aoe) public static bool ShouldUseNoMercy(State state, Strategy strategy) { - if (strategy.NoMercyUse == Strategy.OffensiveAbilityUse.Delay) + if (strategy.NoMercyUse == CommonRotation.Strategy.OffensiveAbilityUse.Delay) { return false; } - else if (strategy.NoMercyUse == Strategy.OffensiveAbilityUse.Force) + else if (strategy.NoMercyUse == CommonRotation.Strategy.OffensiveAbilityUse.Force) { return true; } @@ -382,25 +646,25 @@ public static bool ShouldUseNoMercy(State state, Strategy strategy) public static bool ShouldUseGnash(State state, Strategy strategy) => strategy.GnashUse switch { - Strategy.OffensiveAbilityUse.Delay => false, - Strategy.OffensiveAbilityUse.Force => true, + CommonRotation.Strategy.OffensiveAbilityUse.Delay => false, + CommonRotation.Strategy.OffensiveAbilityUse.Force => true, _ => strategy.CombatTimer >= 0 && state.TargetingEnemy && state.Unlocked(AID.GnashingFang) && state.CD(CDGroup.GnashingFang) < 0.6f && state.Ammo >= 1 }; public static bool ShouldUseZone(State state, Strategy strategy) => strategy.ZoneUse switch { - Strategy.OffensiveAbilityUse.Delay => false, - Strategy.OffensiveAbilityUse.Force => true, + CommonRotation.Strategy.OffensiveAbilityUse.Delay => false, + CommonRotation.Strategy.OffensiveAbilityUse.Force => true, _ => strategy.CombatTimer >= 0 && state.TargetingEnemy && state.Unlocked(AID.SonicBreak) && state.CD(CDGroup.SonicBreak) > state.AnimationLock && state.CD(CDGroup.NoMercy) > 17 }; public static bool ShouldUseFest(State state, Strategy strategy) { - if (strategy.BloodFestUse == Strategy.OffensiveAbilityUse.Delay) + if (strategy.BloodFestUse == CommonRotation.Strategy.OffensiveAbilityUse.Delay) { return false; } - else if (strategy.BloodFestUse == Strategy.OffensiveAbilityUse.Force) + else if (strategy.BloodFestUse == CommonRotation.Strategy.OffensiveAbilityUse.Force) { return true; } @@ -417,8 +681,8 @@ public static bool ShouldUseFest(State state, Strategy strategy) public static bool ShouldUseBow(State state, Strategy strategy) => strategy.BowUse switch { - Strategy.OffensiveAbilityUse.Delay => false, - Strategy.OffensiveAbilityUse.Force => true, + CommonRotation.Strategy.OffensiveAbilityUse.Delay => false, + CommonRotation.Strategy.OffensiveAbilityUse.Force => true, _ => strategy.CombatTimer >= 0 && state.TargetingEnemy && state.Unlocked(AID.BowShock) && state.CD(CDGroup.SonicBreak) > state.AnimationLock && state.CD(CDGroup.NoMercy) > 40 }; @@ -518,6 +782,10 @@ public static AID GetNextBestGCD(State state, Strategy strategy, bool aoe) if (strategy.GaugeStrategy == Strategy.GaugeUse.LightningShotIfNotInMelee && state.RangeToTarget > 3) return AID.LightningShot; + // Lv70 only; can't use Fated Circle (Lv72) sadge + if (aoe && state.Ammo >= 1 && !state.Unlocked(AID.FatedCircle) && !state.Unlocked(AID.DoubleDown) && !state.Unlocked(AID.Bloodfest) && state.Unlocked(AID.BurstStrike) && state.Unlocked(AID.Continuation) && state.CD(CDGroup.GnashingFang) > 24 && state.GunComboStep == 0) + return AID.BurstStrike; + if (state.ReadyToBlast) return state.BestContinuation; if (state.ReadyToGouge) @@ -553,6 +821,18 @@ public static AID GetNextBestGCD(State state, Strategy strategy, bool aoe) if (strategy.GaugeStrategy == Strategy.GaugeUse.Spend) return GetNextAmmoAction(state, strategy, aoe); + if (strategy.GaugeStrategy == Strategy.GaugeUse.Hold) + return GetNextUnlockedComboAction(state, strategy, aoe); + + if (strategy.GaugeStrategy == Strategy.GaugeUse.ForceST) + return GetNextUnlockedComboAction(state, strategy, aoe); + + if (strategy.GaugeStrategy == Strategy.GaugeUse.ForceAOE) + return GetNextUnlockedComboAction(state, strategy, aoe); + + if (strategy.GaugeStrategy == Strategy.GaugeUse.ForceGF) + return GetNextAmmoAction(state, strategy, aoe); + if (strategy.GaugeStrategy == Strategy.GaugeUse.MaxGaugeBeforeDowntime && state.NoMercyLeft < state.AnimationLock) return ChooseRotationBasedOnGauge(state, strategy, aoe); @@ -561,7 +841,6 @@ public static AID GetNextBestGCD(State state, Strategy strategy, bool aoe) public static ActionID GetNextBestOGCD(State state, Strategy strategy, float deadline, bool aoe) { - //bool hasContinuation = state.ReadyToBlast || state.ReadyToGouge || state.ReadyToRip || state.ReadyToTear; if (strategy.SpecialActionUse == Strategy.SpecialAction.LB3) return ActionID.MakeSpell(AID.GunmetalSoul); diff --git a/BossMod/Components/StackSpread.cs b/BossMod/Components/StackSpread.cs index 5a8090f8d9..3cec960b1c 100644 --- a/BossMod/Components/StackSpread.cs +++ b/BossMod/Components/StackSpread.cs @@ -99,6 +99,9 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme // forbid standing next to other stack markers foreach (var stackWith in ActiveStacks.Where(s => s.Target != actor)) hints.AddForbiddenZone(ShapeDistance.Circle(stackWith.Target.Position, stackWith.Radius), stackWith.Activation); + if (Raid.PartyContainsBuddies) // if player got stackmarker and is playing with NPCs, go to a NPC to stack with them + foreach (var stackWith in ActiveStacks.Where(s => s.Target == actor)) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Raid.WithoutSlot().FirstOrDefault(x => !IsStackTarget(x))!.Position, stackWith.Radius - 1), stackWith.Activation); } else if (!IsSpreadTarget(actor)) { diff --git a/BossMod/Config/ConfigUI.cs b/BossMod/Config/ConfigUI.cs index 710bcffa76..b216d949cb 100644 --- a/BossMod/Config/ConfigUI.cs +++ b/BossMod/Config/ConfigUI.cs @@ -1,4 +1,5 @@ using Dalamud.Interface.Utility.Raii; +using System.Diagnostics; using ImGuiNET; using System.Reflection; @@ -68,6 +69,9 @@ public void Draw() using (var tab = ImRaii.TabItem("Slash commands")) if (tab) DrawAvailableCommands(); + using (var tab = ImRaii.TabItem("Information")) + if (tab) + DrawInformation(); } } @@ -77,13 +81,16 @@ public void Draw() { "off", "Disables the AI." }, { "toggle", "Toggles the AI on/off." }, { "targetmaster", "Toggles the focus on target leader." }, - { "follow slot", "Follows the specified slot, eg. Slot1." }, + { "follow slotX", "Follows the specified slot, eg. Slot1." }, { "follow name", "Follows the specified party member by name." }, { "debug", "Toggles the debug menu." }, - { "forbidactions", "Toggles the forbidding of actions." }, + { "forbidactions", "Toggles the forbidding of actions. (only for autorotation)" }, { "forbidmovement", "Toggles the forbidding of movement." }, { "followcombat", "Toggles following during combat." }, - { "followmodule", "Toggles following during active boss module." } + { "followmodule", "Toggles following during active boss module." }, + { "followoutofcombat", "Toggles following during out of combat." }, + { "followtarget", "Toggles following targets during combat." }, + { "positional X", "Switch to positional when following targets. (any, rear, flank, front)" } }; private void DrawAvailableCommands() @@ -96,6 +103,35 @@ private void DrawAvailableCommands() } } + private void DrawInformation() + { + ImGui.Text("Important information"); + ImGui.Separator(); + ImGui.Text("This is a FORK of veyn's BossMod."); + ImGui.Spacing(); + ImGui.Text("Please do not ask him for any support for problems you encounter while using this fork."); + ImGui.Spacing(); + ImGui.Text("Instead visit the Combat Reborn Discord and ask for support there:"); + RenderTextWithLink("https://discord.gg/p54TZMPnC9", "https://discord.gg/p54TZMPnC9"); + } + + static void RenderTextWithLink(string displayText, string url) + { + ImGui.PushID(url); + ImGui.Text(displayText); + if (ImGui.IsItemHovered()) + { + ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } + var textSize = ImGui.CalcTextSize(displayText); + var drawList = ImGui.GetWindowDrawList(); + var cursorPos = ImGui.GetCursorScreenPos(); + drawList.AddLine(cursorPos, new Vector2(cursorPos.X + textSize.X, cursorPos.Y), ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 1, 1))); + ImGui.PopID(); + } + public static void DrawNode(ConfigNode node, ConfigRoot root, UITree tree, WorldState ws) { // draw standard properties diff --git a/BossMod/Data/PartyState.cs b/BossMod/Data/PartyState.cs index 8b92e84f5a..bead5126e6 100644 --- a/BossMod/Data/PartyState.cs +++ b/BossMod/Data/PartyState.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using FFXIVClientStructs.FFXIV.Client.Game.UI; namespace BossMod; @@ -29,6 +30,7 @@ public sealed class PartyState public int LimitBreakCur; public int LimitBreakMax = 10000; + public unsafe bool PartyContainsBuddies => UIState.Instance()->Buddy.DutyHelperInfo.ENpcIds.Length > 0; public PartyState(ActorState actorState) { diff --git a/BossMod/Debug/DebugCollision.cs b/BossMod/Debug/DebugCollision.cs new file mode 100644 index 0000000000..c930753208 --- /dev/null +++ b/BossMod/Debug/DebugCollision.cs @@ -0,0 +1,812 @@ +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/DebugObjects.cs b/BossMod/Debug/DebugObjects.cs index 13d9be2466..4d462fff05 100644 --- a/BossMod/Debug/DebugObjects.cs +++ b/BossMod/Debug/DebugObjects.cs @@ -26,7 +26,7 @@ public unsafe void DrawObjectTable() var internalObj = Utils.GameObjectInternal(obj); var localID = internalObj->LayoutId; - ulong uniqueID = internalObj->GetObjectId(); + ulong uniqueID = internalObj->GetGameObjectId(); 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)) @@ -125,7 +125,7 @@ public static unsafe void DumpObjectTable() { 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.BattleCharaInternal(chara)->GetCastInfo()->CastLocation)}, 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()->TargetLocation)}, 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 bd65c8beec..e2d508c4e0 100644 --- a/BossMod/Debug/DebugParty.cs +++ b/BossMod/Debug/DebugParty.cs @@ -44,8 +44,8 @@ public unsafe void DrawPartyCustom() ImGui.BeginTable("party-custom", 7, ImGuiTableFlags.Resizable); ImGui.TableSetupColumn("Index"); - ImGui.TableSetupColumn("ContentID"); - ImGui.TableSetupColumn("ObjectID"); + ImGui.TableSetupColumn("ContentId"); + ImGui.TableSetupColumn("EntityId"); ImGui.TableSetupColumn("Name"); ImGui.TableSetupColumn("Zone"); ImGui.TableSetupColumn("World"); @@ -58,10 +58,10 @@ public unsafe void DrawPartyCustom() 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; + var id = ui->Buddy.DutyHelperInfo.DutyHelpers[i].EntityId; if (id == 0xE0000000) continue; - var obj = GameObjectManager.Instance()->Objects.GetNetworkedObjectById(id); + var obj = GameObjectManager.Instance()->Objects.GetObjectByEntityId(id); ImGui.TableNextRow(); ImGui.TableNextColumn(); ImGui.TextUnformatted($"B{i}"); @@ -86,7 +86,7 @@ private unsafe void DrawPartyMember(string index, ref PartyMember member) 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($"{member.EntityId:X}"); ImGui.TableNextColumn(); ImGui.TextUnformatted(member.NameString); ImGui.TableNextColumn(); ImGui.TextUnformatted($"{member.TerritoryType}"); ImGui.TableNextColumn(); ImGui.TextUnformatted($"{member.HomeWorld}"); diff --git a/BossMod/Debug/MainDebugWindow.cs b/BossMod/Debug/MainDebugWindow.cs index 372d8cbf26..96bd1a5fde 100644 --- a/BossMod/Debug/MainDebugWindow.cs +++ b/BossMod/Debug/MainDebugWindow.cs @@ -17,6 +17,7 @@ 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,6 +25,7 @@ protected override void Dispose(bool disposing) _debugInput.Dispose(); _debugClassDefinitions.Dispose(); _debugAddon.Dispose(); + _debugCollision.Dispose(); _debugVfx.Dispose(); base.Dispose(disposing); } @@ -134,6 +136,10 @@ public unsafe override void Draw() { DrawWindowSystem(); } + if (ImGui.CollapsingHeader("Collision")) + { + _debugCollision.Draw(); + } if (ImGui.CollapsingHeader("VFX")) { _debugVfx.Draw(); diff --git a/BossMod/Framework/ActionManagerEx.cs b/BossMod/Framework/ActionManagerEx.cs index 58b4f98fbb..550299756b 100644 --- a/BossMod/Framework/ActionManagerEx.cs +++ b/BossMod/Framework/ActionManagerEx.cs @@ -1,8 +1,10 @@ -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Hooking; -using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Character; 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 CSActionType = FFXIVClientStructs.FFXIV.Client.Game.ActionType; namespace BossMod; @@ -10,16 +12,6 @@ namespace BossMod; // handles following features: // 1. automatic action execution (provided by autorotation or ai modules, if enabled); does nothing if no automatic actions are provided // 2. effective animation lock reduction (a-la xivalex) -// game handles instants and casted actions differently: -// * instants: on action request (e.g. on the frame the action button is pressed), animation lock is set to 0.5 (or 0.35 for some specific actions); it then ticks down every frame -// some time later (ping + server latency, typically 50-100ms if ping is good), we receive action effect packet - the packet contains action's animation lock (typically 0.6) -// the game then updates animation lock (now equal to 0.5 minus time since request) to the packet data -// so the 'effective' animation lock between action request and animation lock end is equal to action's animation lock + delay between request and response -// this feature reduces effective animation lock by either removing extra delay completely or clamping it to specified maximal value -// * casts: on action request animation lock is not set (remains equal to 0), remaining cast time is set to action's cast time; remaining cast time then ticks down every frame -// some time later (cast time minus approximately 0.5s, aka slidecast window), we receive action effect packet - the packet contains action's animation lock (typically 0.1) -// the game then updates animation lock (still 0) to the packet data - however, since animation lock isn't ticking down while cast is in progress, there is no extra delay -// this feature does nothing for casts, since they already work correctly // 3. framerate-dependent cooldown reduction // imagine game is running at exactly 100fps (10ms frame time), and action is queued when remaining cooldown is 5ms // on next frame (+10ms), cooldown will be reduced and clamped to 0, action will be executed and it's cooldown set to X ms - so next time it can be pressed at X+10 ms @@ -53,21 +45,17 @@ unsafe sealed class ActionManagerEx : IDisposable 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 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 float EffectiveAnimationLock => _inst->AnimationLock + CastTimeRemaining; // animation lock starts ticking down only when cast ends, so this is the minimal time until next action can be requested public Event ActionRequested = new(); public Event ActionEffectReceived = new(); - public InputOverride InputOverride; - public ActionManagerConfig Config; - public CommonActions.NextAction AutoQueue; // TODO: consider using native 'queue' fields for this? + public AnimationLockTweak AnimLockTweak = new(); + public InputOverride InputOverride = new(); + public ActionManagerConfig Config = Service.Config.Get(); + public CommonActions.NextAction AutoQueue; public bool MoveMightInterruptCast { get; private set; } // if true, moving now might cause cast interruption (for current or queued cast) - private readonly ActionManager* _inst; + private readonly ActionManager* _inst = ActionManager.Instance(); private float _lastReqInitialAnimLock; private int _lastReqSequence = -1; private float _useActionInPast; // if >0 while using an action, cooldown/anim lock will be reduced by this amount as if action was used a bit in the past @@ -77,25 +65,15 @@ unsafe sealed class ActionManagerEx : IDisposable 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 readonly HookAddress _processPacketActionEffectHook; public ActionManagerEx() { - InputOverride = new(); - Config = Service.Config.Get(); - - _inst = ActionManager.Instance(); Service.Log($"[AMEx] ActionManager singleton address = 0x{(ulong)_inst: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}"); + _processPacketActionEffectHook = new(ActionEffectHandler.Addresses.Receive, ProcessPacketActionEffectDetour); } public void Dispose() @@ -113,7 +91,7 @@ public void Dispose() return _inst->GetGroundPositionForCursor(&res) ? res : null; } - public void FaceTarget(Vector3 position, ulong unkObjID = GameObject.InvalidGameObjectId) => _inst->AutoFaceTargetPosition(&position, unkObjID); + public void FaceTarget(Vector3 position, ulong unkObjID = 0xE0000000) => _inst->AutoFaceTargetPosition(&position, unkObjID); public void FaceDirection(WDir direction) { var player = Service.ClientState.LocalPlayer; @@ -172,15 +150,15 @@ public uint GetActionStatus(ActionID action, ulong target, bool checkRecastActiv { if (action.Type is ActionType.BozjaHolsterSlot0 or ActionType.BozjaHolsterSlot1) action = BozjaActionID.GetHolster(action.As()); // see BozjaContentDirector.useFromHolster - return _inst->GetActionStatus((FFXIVClientStructs.FFXIV.Client.Game.ActionType)action.Type, action.ID, target, checkRecastActive, checkCastingActive, outOptExtraInfo); + return _inst->GetActionStatus((CSActionType)action.Type, action.ID, target, checkRecastActive, checkCastingActive, outOptExtraInfo); } // returns time in ms 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); + => ActionManager.GetAdjustedCastTime((CSActionType)action.Type, action.ID, applyProcs, outOptProc); public bool IsRecastTimerActive(ActionID action) - => _inst->IsRecastTimerActive((FFXIVClientStructs.FFXIV.Client.Game.ActionType)action.Type, action.ID); + => _inst->IsRecastTimerActive((CSActionType)action.Type, action.ID); public int GetRecastGroup(ActionID action) => _inst->GetRecastGroup((int)action.Type, action.ID); @@ -200,7 +178,7 @@ private bool ExecuteAction(ActionID action, ulong targetId, Vector3 targetPos) // 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); + return _inst->UseActionLocation((CSActionType)action.Type, action.ID, targetId, &targetPos, extraParam); } } @@ -252,7 +230,7 @@ private void UpdateDetour(ActionManager* self) if (actionAdj != AutoQueue.Action) Service.Log($"[AMEx] Something didn't perform action adjustment correctly: replacing {AutoQueue.Action} with {actionAdj}"); - var targetID = AutoQueue.Target?.InstanceID ?? GameObject.InvalidGameObjectId; + var targetID = AutoQueue.Target?.InstanceID ?? 0xE0000000; var status = GetActionStatus(actionAdj, targetID); if (status == 0) { @@ -277,7 +255,7 @@ private void UpdateDetour(ActionManager* self) InputOverride.UnblockMovement(); } - private bool UseActionLocationDetour(ActionManager* self, FFXIVClientStructs.FFXIV.Client.Game.ActionType actionType, uint actionId, ulong targetId, Vector3* location, uint extraParam) + private bool UseActionLocationDetour(ActionManager* self, CSActionType actionType, uint actionId, ulong targetId, Vector3* location, uint extraParam) { var pc = Service.ClientState.LocalPlayer; var prevSeq = _inst->LastUsedActionSequence; @@ -301,79 +279,70 @@ private bool UseBozjaFromHolsterDirectorDetour(PublicContentBozja* self, uint ho { var currRot = pc?.Rotation ?? 0; var entry = (BozjaHolsterID)self->State.HolsterActions[(int)holsterIndex]; - HandleActionRequest(ActionID.MakeBozjaHolster(entry, (int)slot), 0, GameObject.InvalidGameObjectId, default, prevRot, currRot); + HandleActionRequest(ActionID.MakeBozjaHolster(entry, (int)slot), 0, 0xE0000000, default, prevRot, currRot); } return res; } - private void ProcessPacketActionEffectDetour(uint casterID, FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara* casterObj, Vector3* targetPos, Network.ServerIPC.ActionEffectHeader* header, ulong* effects, ulong* targets) + private void ProcessPacketActionEffectDetour(uint casterID, Character* casterObj, Vector3* targetPos, ActionEffectHandler.Header* header, ActionEffectHandler.TargetEffects* effects, GameObjectId* targets) { - var packetAnimLock = header->animationLockTime; - var action = new ActionID(header->actionType, header->actionId); - + // notify listeners about the event // note: there's a slight difference with dispatching event from here rather than from packet processing (ActionEffectN) functions // 1. action id is already unscrambled // 2. this function won't be called if caster object doesn't exist // the last point is deemed to be minor enough for us to not care, as it simplifies things (no need to hook 5 functions) var info = new ActorCastEvent { - Action = action, - MainTargetID = header->animationTargetId, - AnimationLockTime = header->animationLockTime, + Action = new ActionID((ActionType)header->ActionType, header->ActionId), + MainTargetID = header->AnimationTargetId, + AnimationLockTime = header->AnimationLock, MaxTargets = header->NumTargets, TargetPos = *targetPos, SourceSequence = header->SourceSequence, - GlobalSequence = header->globalEffectCounter, + GlobalSequence = header->GlobalSequence, }; + var rawEffects = (ulong*)effects; for (int i = 0; i < header->NumTargets; ++i) { var targetEffects = new ActionEffects(); for (int j = 0; j < ActionEffects.MaxCount; ++j) - targetEffects[j] = effects[i * 8 + j]; + targetEffects[j] = rawEffects[i * 8 + j]; info.Targets.Add(new(targets[i], targetEffects)); } ActionEffectReceived.Fire(casterID, info); + // call the hooked function and observe the effects + var packetAnimLock = header->AnimationLock; var prevAnimLock = _inst->AnimationLock; _processPacketActionEffectHook.Original(casterID, casterObj, targetPos, header, effects, targets); var currAnimLock = _inst->AnimationLock; - if (casterID != Service.ClientState.LocalPlayer?.ObjectId || header->SourceSequence == 0 && _lastReqSequence != 0) + if (casterID != UIState.Instance()->PlayerState.EntityId || header->SourceSequence == 0 && _lastReqSequence != 0) { - // non-player-initiated; TODO: reconsider the condition for header->SourceSequence == 0 (e.g. autos) - could they happen while we wait for stuff like reholster?.. + // this action is either executed by non-player, or is non-player-initiated + // TODO: reconsider the condition: + // - some actions with SourceSequence != 0 are special-cased in code (NIN's ten/chi/jin) and apparently don't trigger anim-lock, verify + // - actions can have 'force anim lock' flag set, and then trigger anim-lock despite SourceSequence == 0, verify e.g. bozja holster actions + // - auto is the most common cast with SourceSequence == 0; can it happen while waiting for reholster response?.. + // - do we want to do non-anim-lock related things (eg unblock movement override) when we get action with 'force anim lock' flag? if (currAnimLock != prevAnimLock) - Service.Log($"[AMEx] Animation lock updated by non-player-initiated action: #{header->SourceSequence} {casterID:X} {action} {prevAnimLock:f3} -> {currAnimLock:f3}"); + Service.Log($"[AMEx] Animation lock updated by non-player-initiated action: #{header->SourceSequence} {casterID:X} {info.Action} {prevAnimLock:f3} -> {currAnimLock:f3}"); return; } MoveMightInterruptCast = false; // slidecast window start InputOverride.UnblockMovement(); // unblock input unconditionally on successful cast (I assume there are no instances where we need to immediately start next GCD?) + // animation lock delay update float animLockDelay = _lastReqInitialAnimLock - prevAnimLock; float animLockReduction = 0; - - // animation lock delay update if (_lastReqSequence == header->SourceSequence) { if (_lastReqInitialAnimLock > 0) { - float adjDelay = animLockDelay; - if (adjDelay > AnimationLockDelayMax) - { - // sanity check for plugin conflicts - if (header->animationLockTime != packetAnimLock || packetAnimLock % 0.01 is >= 0.0005f and <= 0.0095f) - { - Service.Log($"[AMEx] Unexpected animation lock {packetAnimLock:f} -> {header->animationLockTime:f}, disabling anim lock tweak feature"); - Config.RemoveAnimationLockDelay = false; - } - else - { - animLockReduction = Math.Min(adjDelay - AnimationLockDelayMax, currAnimLock); - adjDelay -= animLockReduction; - _inst->AnimationLock = currAnimLock - animLockReduction; - } - } - AnimationLockDelayAverage = adjDelay * (1 - AnimationLockDelaySmoothing) + AnimationLockDelayAverage * AnimationLockDelaySmoothing; + AnimLockTweak.SanityCheck(packetAnimLock, header->AnimationLock); + animLockReduction = AnimLockTweak.Apply(_inst->AnimationLock, animLockDelay); + _inst->AnimationLock -= animLockReduction; } } else if (currAnimLock != prevAnimLock) @@ -381,7 +350,7 @@ private void ProcessPacketActionEffectDetour(uint casterID, FFXIVClientStructs.F Service.Log($"[AMEx] Animation lock updated by action with unexpected sequence ID #{header->SourceSequence}: {prevAnimLock:f3} -> {currAnimLock:f3}"); } - Service.Log($"[AMEx] AEP #{header->SourceSequence} {prevAnimLock:f3} {action} -> ALock={currAnimLock:f3} (delayed by {animLockDelay:f3}-{animLockReduction:f3}), CTR={CastTimeRemaining:f3}, GCD={GCD():f3}"); + Service.Log($"[AMEx] AEP #{header->SourceSequence} {prevAnimLock:f3} {info.Action} -> ALock={currAnimLock:f3} (delayed by {animLockDelay:f3}-{animLockReduction:f3}), CTR={CastTimeRemaining:f3}, GCD={GCD():f3}"); _lastReqSequence = -1; } diff --git a/BossMod/Framework/Plugin.cs b/BossMod/Framework/Plugin.cs index 106c0ca00c..9ac8cf96b6 100644 --- a/BossMod/Framework/Plugin.cs +++ b/BossMod/Framework/Plugin.cs @@ -163,6 +163,6 @@ private void DrawUI() private void OnConditionChanged(ConditionFlag flag, bool value) { - Service.Log($"Condition chage: {flag}={value}"); + Service.Log($"Condition change: {flag}={value}"); } } diff --git a/BossMod/Framework/Service.cs b/BossMod/Framework/Service.cs index 75e24e0e3b..a3499cfae4 100644 --- a/BossMod/Framework/Service.cs +++ b/BossMod/Framework/Service.cs @@ -26,6 +26,7 @@ public sealed class Service [PluginService] public static IFramework Framework { get; private set; } [PluginService] public static ITextureProvider Texture { get; private set; } [PluginService] public static ICommandManager CommandManager { get; private set; } + [PluginService] public static IDtrBar DtrBar { get; private set; } [PluginService] public static DalamudPluginInterface PluginInterface { get; private set; } #pragma warning restore CS8618 diff --git a/BossMod/Framework/WorldStateGameSync.cs b/BossMod/Framework/WorldStateGameSync.cs index 079e69bcdc..6176c57202 100644 --- a/BossMod/Framework/WorldStateGameSync.cs +++ b/BossMod/Framework/WorldStateGameSync.cs @@ -169,7 +169,7 @@ private unsafe void UpdateActors() for (int i = 0; i < _actorsByIndex.Length; ++i) { var actor = _actorsByIndex[i]; - var obj = mgr->Objects.All[i].Value; + var obj = mgr->Objects.IndexSorted[i].Value; if (obj != null && obj->EntityId == InvalidEntityId) obj = null; // ignore non-networked objects (really?..) @@ -282,9 +282,9 @@ private unsafe void UpdateActor(GameObject* obj, int index, Actor? act) ? new ActorCastInfo { Action = new((ActionType)castInfo->ActionType, castInfo->ActionId), - TargetID = SanitizedObjectID(castInfo->CastTargetId), + TargetID = SanitizedObjectID(castInfo->TargetId), Rotation = chr->CastRotation.Radians(), - Location = castInfo->CastLocation, + Location = castInfo->TargetLocation, 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, @@ -353,7 +353,7 @@ private unsafe void UpdateParty() var ui = UIState.Instance(); // update player slot - UpdatePartySlot(PartyState.PlayerSlot, UIState.Instance()->PlayerState.ContentId, UIState.Instance()->PlayerState.ObjectId); + UpdatePartySlot(PartyState.PlayerSlot, UIState.Instance()->PlayerState.ContentId, UIState.Instance()->PlayerState.EntityId); // update normal party slots: first update/remove existing members, then add new ones for (int i = PartyState.PlayerSlot + 1; i < PartyState.MaxPartySize; ++i) @@ -365,7 +365,7 @@ private unsafe void UpdateParty() // 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 + UpdatePartySlot(i, contentID, member->EntityId); // 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 } @@ -382,12 +382,12 @@ private unsafe void UpdateParty() { ref var member = ref gm->PartyMembers[i]; if (_ws.Party.ContentIDs.IndexOf(member.ContentId) == -1) - AddPartyMember(member.ContentId, member.ObjectId); + AddPartyMember(member.ContentId, member.EntityId); // 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; + var instanceID = ui->Buddy.DutyHelperInfo.DutyHelpers[i].EntityId; if (instanceID != InvalidEntityId && _ws.Party.ActorIDs[1..PartyState.MaxPartySize].IndexOf(instanceID) == -1) AddPartyMember(0, instanceID); // else: buddy is non-existent or already updated, skip @@ -400,7 +400,7 @@ private unsafe void UpdateParty() 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); + UpdatePartySlot(i, 0, member != null ? member->EntityId : 0); } // update limit break @@ -413,7 +413,7 @@ 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) + if (ui->Buddy.DutyHelperInfo.DutyHelpers[i].EntityId == instanceID) return true; return false; } diff --git a/BossMod/Modules/Endwalker/Dungeon/D11LapisManalis/D111Albion.cs b/BossMod/Modules/Endwalker/Dungeon/D11LapisManalis/D111Albion.cs index c2cb13b147..f204774170 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D11LapisManalis/D111Albion.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D11LapisManalis/D111Albion.cs @@ -41,120 +41,119 @@ public enum IconID : uint class WildlifeCrossing(BossModule module) : Components.GenericAOEs(module) { private static readonly AOEShapeRect rect = new(20, 5, 20); - private static readonly Angle _rot90 = 90.Degrees(); - private static readonly Angle _rotM90 = -90.Degrees(); - private (bool active, WPos position, Angle rotation, int count, DateTime reset, List beasts) stampede1; - private (bool active, WPos position, Angle rotation, int count, DateTime reset, List beasts) stampede2; + private static readonly Angle Rot90 = 90.Degrees(); + private static readonly Angle RotM90 = -90.Degrees(); + + private (bool Active, WPos Position, Angle Rotation, int Count, DateTime Reset, List Beasts) stampede1; + private (bool Active, WPos Position, Angle Rotation, int Count, DateTime Reset, List Beasts) stampede2; + private readonly (bool, WPos, Angle, int, DateTime, List)[] stampedePositions = [ - (true, new WPos(4, -759), _rot90, 0, default, []), - (true, new WPos(44, -759), _rotM90, 0, default, []), - (true, new WPos(4, -749), _rot90, 0, default, []), - (true, new WPos(44, -749), _rotM90, 0, default, []), - (true, new WPos(4, -739), _rot90, 0, default, []), - (true, new WPos(44, -739), _rotM90, 0, default, []), - (true, new WPos(4, -729), _rot90, 0, default, []), - (true, new WPos(44, -729), _rotM90, 0, default, []), + (true, new WPos(4, -759), Rot90, 0, default, []), + (true, new WPos(44, -759), RotM90, 0, default, []), + (true, new WPos(4, -749), Rot90, 0, default, []), + (true, new WPos(44, -749), RotM90, 0, default, []), + (true, new WPos(4, -739), Rot90, 0, default, []), + (true, new WPos(44, -739), RotM90, 0, default, []), + (true, new WPos(4, -729), Rot90, 0, default, []), + (true, new WPos(44, -729), RotM90, 0, default, []) ]; - private bool Newstampede => stampede1 == default; + + private bool IsNewStampede => stampede1 == default; public override IEnumerable ActiveAOEs(int slot, Actor actor) { - if (stampede1.active && stampede1.beasts.Count > 0) - yield return new(new AOEShapeRect(CalculateStampedeLength(stampede1.beasts) + 30, 5), new(stampede1.beasts[^1].Position.X, stampede1.position.Z), stampede1.rotation); - if (stampede2.active && stampede2.beasts.Count > 0) - yield return new(new AOEShapeRect(CalculateStampedeLength(stampede2.beasts) + 30, 5), new(stampede2.beasts[^1].Position.X, stampede2.position.Z), stampede2.rotation); - if (stampede1.active && stampede1.beasts.Count == 0) - yield return new(rect, stampede1.position, _rot90); - if (stampede2.active && stampede2.beasts.Count == 0) - yield return new(rect, stampede2.position, _rot90); + if (stampede1.Active && stampede1.Beasts.Count > 0) + yield return CreateAOEInstance(stampede1); + if (stampede2.Active && stampede2.Beasts.Count > 0) + yield return CreateAOEInstance(stampede2); + if (stampede1.Active && stampede1.Beasts.Count == 0) + yield return new(rect, stampede1.Position, Rot90); + if (stampede2.Active && stampede2.Beasts.Count == 0) + yield return new(rect, stampede2.Position, Rot90); + } + + private static AOEInstance CreateAOEInstance((bool Active, WPos Position, Angle Rotation, int Count, DateTime Reset, List Beasts) stampede) + { + var length = CalculateStampedeLength(stampede.Beasts) + 30; + var position = new WPos(stampede.Beasts[^1].Position.X, stampede.Position.Z); + return new(new AOEShapeRect(length, 5), position, stampede.Rotation); } private static float CalculateStampedeLength(List beasts) => (beasts[0].Position - beasts[^1].Position).Length(); public override void OnEventEnvControl(byte index, uint state) { - if (state == 0x00020001) + if (state != 0x00020001) + return; + var stampedePosition = GetStampedePosition(index); + if (stampedePosition == null) + return; + if (IsNewStampede) + stampede1 = stampedePosition.Value; + else + stampede2 = stampedePosition.Value; + } + + private (bool, WPos, Angle, int, DateTime, List)? GetStampedePosition(byte index) + { + return index switch { - if (index == 0x21) - if (Newstampede) - stampede1 = stampedePositions[0]; - else - stampede2 = stampedePositions[0]; - if (index == 0x25) - if (Newstampede) - stampede1 = stampedePositions[1]; - else - stampede2 = stampedePositions[1]; - if (index == 0x22) - if (Newstampede) - stampede1 = stampedePositions[2]; - else - stampede2 = stampedePositions[2]; - if (index == 0x26) - if (Newstampede) - stampede1 = stampedePositions[3]; - else - stampede2 = stampedePositions[3]; - if (index == 0x23) - if (Newstampede) - stampede1 = stampedePositions[4]; - else - stampede2 = stampedePositions[4]; - if (index == 0x27) - if (Newstampede) - stampede1 = stampedePositions[5]; - else - stampede2 = stampedePositions[5]; - if (index == 0x24) - if (Newstampede) - stampede1 = stampedePositions[6]; - else - stampede2 = stampedePositions[6]; - if (index == 0x28) - if (Newstampede) - stampede1 = stampedePositions[7]; - else - stampede2 = stampedePositions[7]; - } + 0x21 => stampedePositions[0], + 0x25 => stampedePositions[1], + 0x22 => stampedePositions[2], + 0x26 => stampedePositions[3], + 0x23 => stampedePositions[4], + 0x27 => stampedePositions[5], + 0x24 => stampedePositions[6], + 0x28 => stampedePositions[7], + _ => default + }; } public override void Update() { - var stampede1Position = new WPos(24, stampede1.position.Z); - var stampede2Position = new WPos(24, stampede2.position.Z); + UpdateStampede(ref stampede1, new WPos(24, stampede1.Position.Z)); + UpdateStampede(ref stampede2, new WPos(24, stampede2.Position.Z)); + ResetStampedeIfNeeded(ref stampede1); + ResetStampedeIfNeeded(ref stampede2); + } + + private void UpdateStampede(ref (bool Active, WPos Position, Angle Rotation, int Count, DateTime Reset, List Beasts) stampede, WPos position) + { foreach (var oid in new[] { OID.WildBeasts4, OID.WildBeasts3, OID.WildBeasts2, OID.WildBeasts1 }) { var beasts = Module.Enemies(oid); foreach (var b in beasts) { - if (b.Position.InRect(stampede1Position, stampede1.rotation, 33, 33, 5) && !stampede1.beasts.Contains(b)) - stampede1.beasts.Add(b); - if (b.Position.InRect(stampede2Position, stampede2.rotation, 33, 33, 5) && !stampede2.beasts.Contains(b)) - stampede2.beasts.Add(b); + if (b.Position.InRect(position, stampede.Rotation, 33, 33, 5) && !stampede.Beasts.Contains(b)) + stampede.Beasts.Add(b); } } + } - if (stampede1.reset != default && WorldState.CurrentTime > stampede1.reset) - stampede1 = default; - if (stampede2.reset != default && WorldState.CurrentTime > stampede2.reset) - stampede2 = default; + private void ResetStampedeIfNeeded(ref (bool Active, WPos Position, Angle Rotation, int Count, DateTime Reset, List Beasts) stampede) + { + if (stampede.Reset != default && WorldState.CurrentTime > stampede.Reset) + stampede = default; } public override void OnEventCast(Actor caster, ActorCastEvent spell) { - if ((AID)spell.Action.ID == AID.WildlifeCrossing) - { - if (MathF.Abs(caster.Position.Z - stampede1.position.Z) < 1) - ++stampede1.count; - if (MathF.Abs(caster.Position.Z - stampede2.position.Z) < 1) - ++stampede2.count; - if (stampede1.count == 30) // sometimes stampedes only have 30 instead of 31 hits for some reason, so we take the lower value and add a 0.5s reset timer via update - stampede1.reset = WorldState.FutureTime(0.5f); - if (stampede2.count == 30) - stampede1.reset = WorldState.FutureTime(0.5f); - } + if ((AID)spell.Action.ID != AID.WildlifeCrossing) + return; + UpdateStampedeCount(ref stampede1, caster.Position.Z); + UpdateStampedeCount(ref stampede2, caster.Position.Z); + } + + private void UpdateStampedeCount(ref (bool Active, WPos Position, Angle Rotation, int Count, DateTime Reset, List Beasts) stampede, float casterZ) + { + if (MathF.Abs(casterZ - stampede.Position.Z) < 1) + ++stampede.Count; + if (stampede.Count != 30) + return; + stampede.Reset = WorldState.FutureTime(0.5f); } } @@ -223,7 +222,8 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) } } -class IcyThroes2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.IcyThroes4), new AOEShapeCircle(6)); +class IcyThroes2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.IcyThroes2), new AOEShapeCircle(6)); +class IcyThroes4(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.IcyThroes4), new AOEShapeCircle(6)); class KnockOnIce(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.KnockOnIce2), new AOEShapeCircle(5)); class RightSlam(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RightSlam), new AOEShapeRect(20, 80, 0, -90.Degrees())); //full width = half width in this case + angle is detected incorrectly, length and width are also switched class LeftSlam(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LeftSlam), new AOEShapeRect(20, 80, 0, 90.Degrees())); //full width = half width in this case + angle is detected incorrectly, length and width are also switched @@ -247,6 +247,7 @@ public D111AlbionStates(BossModule module) : base(module) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter(); } } diff --git a/BossMod/Modules/Endwalker/Dungeon/D12Aetherfont/D121Lyngbakr.cs b/BossMod/Modules/Endwalker/Dungeon/D12Aetherfont/D121Lyngbakr.cs index d73b4fa975..ebb77961eb 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D12Aetherfont/D121Lyngbakr.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D12Aetherfont/D121Lyngbakr.cs @@ -94,4 +94,4 @@ public class D121Lyngbakr(WorldState ws, Actor primary) : BossModule(ws, primary private static readonly List difference = [new Rectangle(new(-322, 99), 20, 2.25f), new Rectangle(new(-322, 140), 20, 1.25f)]; public static readonly ArenaBounds arena = new ArenaBoundsComplex(union, difference); -} \ No newline at end of file +} diff --git a/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D131DarkElf.cs b/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D131DarkElf.cs index f68973f944..d6b8384c38 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D131DarkElf.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D131DarkElf.cs @@ -114,23 +114,11 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (Module.FindComponent()!.Stacks.Count == 0) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D131DarkElfStates : StateMachineBuilder { public D131DarkElfStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D132DamcyanAntilon.cs b/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D132DamcyanAntlion.cs similarity index 86% rename from BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D132DamcyanAntilon.cs rename to BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D132DamcyanAntlion.cs index 921f502d03..7f4902089e 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D132DamcyanAntilon.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D132DamcyanAntlion.cs @@ -1,4 +1,4 @@ -namespace BossMod.Endwalker.Dungeon.D13LunarSubterrane.D132DamcyanAntilon; +namespace BossMod.Endwalker.Dungeon.D13LunarSubterrane.D132DamcyanAntlion; public enum OID : uint { @@ -35,7 +35,7 @@ class SandblastVoidzone(BossModule module) : Components.GenericAOEs(module) public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; public override void OnCastStarted(Actor caster, ActorCastInfo spell) { - if ((AID)spell.Action.ID == AID.Sandblast && Module.Arena.Bounds == D132DamcyanAntilon.startingBounds) + if ((AID)spell.Action.ID == AID.Sandblast && Module.Arena.Bounds == D132DamcyanAntlion.startingBounds) { _aoes.Add(new(rect, Module.Center + new WDir(0, -22.5f), 90.Degrees(), spell.NPCFinishAt)); _aoes.Add(new(rect, Module.Center + new WDir(0, 22.5f), 90.Degrees(), spell.NPCFinishAt)); @@ -45,7 +45,7 @@ public override void OnEventEnvControl(byte index, uint state) { if (state == 0x00020001 && index == 0x00) { - Module.Arena.Bounds = D132DamcyanAntilon.defaultBounds; + Module.Arena.Bounds = D132DamcyanAntlion.defaultBounds; _aoes.Clear(); } } @@ -195,29 +195,21 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users +class StayInBounds(BossModule module) : BossComponent(module) { public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (!Service.Config.Get().Enabled) - { - if (!Module.InBounds(actor.Position)) // return into module bounds if accidently left bounds - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center, 3)); - else if (!Module.FindComponent()!.Sources(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && Module.FindComponent()!.Stacks.Count == 0 && - !Module.FindComponent()!.TowerDanger) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } + if (!Module.InBounds(actor.Position)) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center, 3)); } } -class D132DamcyanAntilonStates : StateMachineBuilder +class D132DamcyanAntlionStates : StateMachineBuilder { - public D132DamcyanAntilonStates(BossModule module) : base(module) + public D132DamcyanAntlionStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() @@ -229,8 +221,8 @@ public D132DamcyanAntilonStates(BossModule module) : base(module) } } -[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "Malediktus", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 823, NameID = 12484)] -public class D132DamcyanAntilon(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 60), startingBounds) +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "Malediktus", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 823, NameID = 12484)] +public class D132DamcyanAntlion(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 60), startingBounds) { public static readonly ArenaBounds startingBounds = new ArenaBoundsRect(19.5f, 25); public static readonly ArenaBounds defaultBounds = new ArenaBoundsRect(19.5f, 20); diff --git a/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D133Durante.cs b/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D133Durante.cs index cf006e06a5..13631e5166 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D133Durante.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D133Durante.cs @@ -146,21 +146,12 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) { } class DeathsJourney(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DeathsJourney), new AOEShapeCircle(8)); class DeathsJourney2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DeathsJourney2), new AOEShapeCone(30, 15.Degrees())); -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users +class StayInBounds(BossModule module) : BossComponent(module) { public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (!Service.Config.Get().Enabled) - { - if (!Module.InBounds(actor.Position)) // return into module bounds if accidently left bounds - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center, 3)); - else if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && Module.FindComponent()!.Spreads.Count == 0 && - Module.FindComponent()!.CurrentBaits.Count == 0 && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } + if (!Module.InBounds(actor.Position)) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center, 3)); } } @@ -169,7 +160,7 @@ class D133DuranteStates : StateMachineBuilder public D133DuranteStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/RealmReborn/Trial/T04PortaDecumana/T04PortaDecumana1.cs b/BossMod/Modules/RealmReborn/Trial/T04PortaDecumana/T04PortaDecumana1.cs index 8f3f572850..8650a944f0 100644 --- a/BossMod/Modules/RealmReborn/Trial/T04PortaDecumana/T04PortaDecumana1.cs +++ b/BossMod/Modules/RealmReborn/Trial/T04PortaDecumana/T04PortaDecumana1.cs @@ -76,26 +76,11 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class T04PortaDecumana1States : StateMachineBuilder { public T04PortaDecumana1States(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/RealmReborn/Trial/T04PortaDecumana/T04PortaDecumana2.cs b/BossMod/Modules/RealmReborn/Trial/T04PortaDecumana/T04PortaDecumana2.cs index ccb04c68f1..98c51140e7 100644 --- a/BossMod/Modules/RealmReborn/Trial/T04PortaDecumana/T04PortaDecumana2.cs +++ b/BossMod/Modules/RealmReborn/Trial/T04PortaDecumana/T04PortaDecumana2.cs @@ -125,21 +125,6 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && Module.FindComponent()!.Spreads.Count == 0 && - Module.FindComponent()!.Stacks.Count == 0 && !Module.FindComponent()!.ActiveOrbs.Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class Ultima(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.Ultima), "Enrage!", true); class T04PortaDecumana2States : StateMachineBuilder @@ -147,7 +132,6 @@ class T04PortaDecumana2States : StateMachineBuilder public T04PortaDecumana2States(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D011ForgivenDissonance.cs b/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D011ForgivenDissonance.cs index f994a343d4..28171d8100 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D011ForgivenDissonance.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D011ForgivenDissonance.cs @@ -29,25 +29,11 @@ class ThePathofLight(BossModule module) : Components.RaidwideCast(module, Action class WoodenHorse(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WoodenHorse), new AOEShapeCone(40, 45.Degrees())); class Pillory(BossModule module) : Components.SingleTargetDelayableCast(module, ActionID.MakeSpell(AID.Pillory)); -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D011ForgivenDissonanceStates : StateMachineBuilder { public D011ForgivenDissonanceStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D012TesleentheForgiven.cs b/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D012TesleentheForgiven.cs index 859912c457..7b8b0eebb4 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D012TesleentheForgiven.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D012TesleentheForgiven.cs @@ -68,23 +68,11 @@ public override void OnEventIcon(Actor actor, uint iconID) class Exorcise(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.ExorciseA), 6); class HolyWater(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 6, ActionID.MakeSpell(AID.HolyWater), m => m.Enemies(OID.HolyWaterVoidzone).Where(z => z.EventState != 7), 0.8f); -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (Module.FindComponent()!.CurrentBaits.Count == 0 && Module.FindComponent()!.Stacks.Count == 0 && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D012TesleentheForgivenStates : StateMachineBuilder { public D012TesleentheForgivenStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D013Philia.cs b/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D013Philia.cs index 7052bee167..765570d638 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D013Philia.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D013Philia.cs @@ -97,6 +97,12 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme OID.Boss => -1, _ => 0 }; + if (!Service.Config.Get().Enabled) + { + var ironchain = Module.Enemies(OID.IronChain).FirstOrDefault(); + if (ironchain != null && !ironchain.IsDead) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(ironchain.Position, ironchain.HitboxRadius + 3)); + } } public override void OnEventIcon(Actor actor, uint iconID) @@ -332,32 +338,11 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && Module.FindComponent()!.CurrentBaits.Count == 0 && - Module.FindComponent()!.Spreads.Count == 0 && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.targets.Contains(actor)) - if (actor.Role is Role.Melee or Role.Tank) - { - var ironchain = Module.Enemies(OID.IronChain).FirstOrDefault(); - if (ironchain != null && !ironchain.IsDead) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(ironchain.Position, ironchain.HitboxRadius + 3)); - else - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } - } -} - class D013PhiliaStates : StateMachineBuilder { public D013PhiliaStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D021AencThon.cs b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D021AencThon.cs index 16ba936e20..127e6fd824 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D021AencThon.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D021AencThon.cs @@ -94,23 +94,11 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && Module.FindComponent()!.Stacks.Count == 0) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D021AencThonStates : StateMachineBuilder { public D021AencThonStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D022Griaule.cs b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D022Griaule.cs index eeb4cf9734..3bb0b08027 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D022Griaule.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D022Griaule.cs @@ -69,23 +69,11 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.Active) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D022GriauleStates : StateMachineBuilder { public D022GriauleStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter(); diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D023AencThon.cs b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D023AencThon.cs index 5eb0491418..d1b0bbc44f 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D023AencThon.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D023AencThon.cs @@ -153,24 +153,11 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.Active && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D033AencThonStates : StateMachineBuilder { public D033AencThonStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D030RonkanDreamer.cs b/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D030RonkanDreamer.cs index 1d3b19a89b..063c43f235 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D030RonkanDreamer.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D030RonkanDreamer.cs @@ -87,17 +87,6 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class BurningBeam(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BurningBeam), new AOEShapeRect(15, 2)); class D030RonkanDreamerStates : StateMachineBuilder @@ -105,7 +94,6 @@ class D030RonkanDreamerStates : StateMachineBuilder public D030RonkanDreamerStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D031Lozatl.cs b/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D031Lozatl.cs index 60f2d38d04..225cecb3ec 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D031Lozatl.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D031Lozatl.cs @@ -51,24 +51,11 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D031LozatlStates : StateMachineBuilder { public D031LozatlStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D032Batsquatch.cs b/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D032Batsquatch.cs index 274c3af38d..6c52fe9378 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D032Batsquatch.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D032Batsquatch.cs @@ -27,24 +27,11 @@ class RipperFang(BossModule module) : Components.SingleTargetDelayableCast(modul class FallingRock(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FallingRock), new AOEShapeCircle(3)); class FallingRock2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FallingRock2), new AOEShapeCircle(2)); -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D032BatsquatchStates : StateMachineBuilder { public D032BatsquatchStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D033Eros.cs b/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D033Eros.cs index a5c2c38743..4b27bcc0c9 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D033Eros.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D033Eros.cs @@ -194,26 +194,11 @@ class HeavingBreath(BossModule module) : Components.KnockbackFromCastTarget(modu class Glossolalia(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Glossolalia)); class Rend(BossModule module) : Components.SingleTargetDelayableCast(module, ActionID.MakeSpell(AID.Rend)); -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.targets.Contains(actor) && !Module.FindComponent()!.targets.Contains(actor) && - !Module.FindComponent()!.targets.Contains(actor) && Module.FindComponent()!.Stacks.Count == 0 && - Module.FindComponent()!.Spreads.Count == 0 && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D033ErosStates : StateMachineBuilder { public D033ErosStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D04MalikahsWell/D041GreaterArmadillo.cs b/BossMod/Modules/Shadowbringers/Dungeon/D04MalikahsWell/D041GreaterArmadillo.cs index abfbec4ee1..4a14000fc4 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D04MalikahsWell/D041GreaterArmadillo.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D04MalikahsWell/D041GreaterArmadillo.cs @@ -50,24 +50,11 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && Module.FindComponent()!.Stacks.Count == 0) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D041GreaterArmadilloStates : StateMachineBuilder { public D041GreaterArmadilloStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D04MalikahsWell/D042AmphibiousTalos.cs b/BossMod/Modules/Shadowbringers/Dungeon/D04MalikahsWell/D042AmphibiousTalos.cs index bbb3cff2db..ab80e7e9e8 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D04MalikahsWell/D042AmphibiousTalos.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D04MalikahsWell/D042AmphibiousTalos.cs @@ -80,26 +80,13 @@ class HighPressureRaidwide(BossModule module) : Components.RaidwideCast(module, class HighPressureKnockback(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.HighPressure), 20, stopAtWall: true); class GeyserEruption(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GeyserEruption), new AOEShapeCircle(8)); class Geysers(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(OID.Geyser).Where(v => v.EventState != 7)); - class Wellbore(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Wellbore), new AOEShapeCircle(15)); -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D042AmphibiousTalosStates : StateMachineBuilder { public D042AmphibiousTalosStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D04MalikahsWell/D043Storge.cs b/BossMod/Modules/Shadowbringers/Dungeon/D04MalikahsWell/D043Storge.cs index 3660d6bda8..50b6d740e5 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D04MalikahsWell/D043Storge.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D04MalikahsWell/D043Storge.cs @@ -82,23 +82,11 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D043StorgeStates : StateMachineBuilder { public D043StorgeStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D051ForgivenCruelty.cs b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D051ForgivenCruelty.cs index af97e6535c..0b77cf0bfd 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D051ForgivenCruelty.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D051ForgivenCruelty.cs @@ -29,23 +29,12 @@ class CycloneWing(BossModule module) : Components.RaidwideCast(module, ActionID. class HurricaneWing(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HurricaneWing), new AOEShapeCircle(10)); class TyphoonWing(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TyphoonWing), new AOEShapeCone(25, 30.Degrees())); class TyphoonWing2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TyphoonWing2), new AOEShapeCone(25, 30.Degrees())); -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} class D051ForgivenCrueltyStates : StateMachineBuilder { public D051ForgivenCrueltyStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D053ForgivenWhimsy.cs b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D053ForgivenWhimsy.cs index d683849885..ae342d7f11 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D053ForgivenWhimsy.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D053ForgivenWhimsy.cs @@ -114,23 +114,11 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && Module.FindComponent()!.Towers.Count == 0) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D053ForgivenWhimsyStates : StateMachineBuilder { public D053ForgivenWhimsyStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D054ForgivenRevelry.cs b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D054ForgivenRevelry.cs index 77d7009e00..32d3e9a8b5 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D054ForgivenRevelry.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D054ForgivenRevelry.cs @@ -47,23 +47,12 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) } class LightShot(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LightShot), new AOEShapeRect(40, 2)); -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} class D054ForgivenRevelryStates : StateMachineBuilder { public D054ForgivenRevelryStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter(); } diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D055ForgivenObscenity.cs b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D055ForgivenObscenity.cs index 4be8bf1757..716adf9cc0 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D055ForgivenObscenity.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D055ForgivenObscenity.cs @@ -224,23 +224,11 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank && Module.PrimaryActor.IsTargetable) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D055ForgivenObscenityStates : StateMachineBuilder { public D055ForgivenObscenityStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D061TheFirstBeast.cs b/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D061TheFirstBeast.cs index 9898df064a..d0cfab3370 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D061TheFirstBeast.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D061TheFirstBeast.cs @@ -86,25 +86,11 @@ class TheFinalSky(BossModule module) : Components.CastLineOfSightAOE(module, Act public override IEnumerable BlockerActors() => Module.Enemies(OID.FallenStar); } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - Module.FindComponent()!.Origin == null && Module.FindComponent()!.CurrentBaits.Count == 0 && Module.FindComponent()!.Stacks.Count == 0) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D061FirstBeastStates : StateMachineBuilder { public D061FirstBeastStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D062Bellwether.cs b/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D062Bellwether.cs index 4ae5e66985..1093d7fcc8 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D062Bellwether.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D062Bellwether.cs @@ -36,22 +36,11 @@ class Comet(BossModule module) : Components.LocationTargetedAOEs(module, ActionI class SicklyInferno(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SicklyInferno), 5); class Burst(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.BurstEnrage), "Enrage!", true); -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (!Service.Config.Get().Enabled) - if (actor.Role is Role.Melee or Role.Tank && Module.PrimaryActor.IsTargetable) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } -} - class D062BellwetherStates : StateMachineBuilder { public D062BellwetherStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D063Therion.cs b/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D063Therion.cs index cd7a70ed1d..783eb81e52 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D063Therion.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D063Therion.cs @@ -221,19 +221,12 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) } } -class MeleeRange(BossModule module) : BossComponent(module) // force melee range for melee rotation solver users +class StayInBounds(BossModule module) : BossComponent(module) { public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (!Service.Config.Get().Enabled) - { - if (!Module.InBounds(actor.Position)) // return into module bounds if accidently walked into fire to prevent death by doom - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center, 3)); - else if (!Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any()) - if (actor.Role is Role.Melee or Role.Tank) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); - } + if (!Module.InBounds(actor.Position)) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center, 3)); } } @@ -242,7 +235,7 @@ class D063TherionStates : StateMachineBuilder public D063TherionStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Network/ServerIPC.cs b/BossMod/Network/ServerIPC.cs index c8c701f283..9eb79562e1 100644 --- a/BossMod/Network/ServerIPC.cs +++ b/BossMod/Network/ServerIPC.cs @@ -12,6 +12,7 @@ namespace BossMod.Network.ServerIPC; // SystemLogMessage1: FFXIVOpcodes = SomeDirectorUnk4 // WaymarkPreset: FFXIVOpcodes = PlaceFieldMarkerPreset, machina = PresetWaymark // Waymark: FFXIVOpcodes = PlaceFieldMarker +// ActorCustomizeData: PlayerUpdateLook // actor control examples: normal = toggle weapon, self = cooldown, target = target change public enum PacketID { @@ -98,7 +99,7 @@ public enum PacketID Transfer = 266, ActorSetPos = 267, ActorCast = 269, - PlayerUpdateLook = 270, + ActorCustomizeData = 270, UpdateParty = 271, InitZone = 272, ApplyIDScramble = 273, @@ -114,6 +115,7 @@ public enum PacketID FirstAttack = 283, PlayerStateFlags = 284, PlayerClassInfo = 285, + PlayerBlueMageActions = 286, ModelEquip = 287, Examine = 288, CharaNameReq = 291, diff --git a/FFXIVClientStructs b/FFXIVClientStructs index 44fb68a549..705119e450 160000 --- a/FFXIVClientStructs +++ b/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 44fb68a549f6e2a4b2674a7e9687cd2054de5f36 +Subproject commit 705119e450a671a76166cec525364414380c5e90