From 0e8ca86864bb7bf4abdb844d9cdca2bc4ebc08ee Mon Sep 17 00:00:00 2001 From: Akechiiiii <167795370+Akechiiiii@users.noreply.github.com> Date: Mon, 3 Jun 2024 04:02:50 -0700 Subject: [PATCH 01/10] GNBRotation small "Gauge" update Added a couple options within "Gauge" 1. Hold Carts (optimally, works for ST and AOE) 2. Spend now uses Double Down, Gnashing Fang combo, and Burst Strike. Will use combo if no carts. [was Gnashing Fang (no combo) > Burst Strike (no Double Down)] --- BossMod/Autorotation/GNB/GNBRotation.cs | 141 ++++++++++++++++++++++-- 1 file changed, 130 insertions(+), 11 deletions(-) diff --git a/BossMod/Autorotation/GNB/GNBRotation.cs b/BossMod/Autorotation/GNB/GNBRotation.cs index bcbe7c754f..4ff0cbd08f 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 @@ -34,6 +34,7 @@ public override string ToString() } // strategy configuration + // TODO: add in Hold Double Down; Force ST or AOE combo public class Strategy : CommonRotation.Strategy { public enum GaugeUse : uint @@ -43,17 +44,20 @@ public enum GaugeUse : uint [PropertyDisplay("Spend all gauge ASAP", 0x8000ff00)] Spend = 1, // spend all gauge asap, don't bother conserving + [PropertyDisplay("Hold Carts", 0x800000ff)] + Hold = 2, // Hold carts + [PropertyDisplay("Use Lightning Shot if outside melee", 0x80c08000)] - LightningShotIfNotInMelee = 2, + LightningShotIfNotInMelee = 3, [PropertyDisplay("Use ST combo if still in ST combo, else use AOE combo", 0x80c0c000)] - ComboFitBeforeDowntime = 3, // useful on late phases before downtime + ComboFitBeforeDowntime = 4, // 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 = 5, // 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 = 6, // useful for ensuring ST extension is used right before long downtime } public enum PotionUse : uint @@ -245,12 +249,68 @@ 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; } + // todo: Add Hold Double Down only? if (Service.Config.Get().Skscheck && state.Ammo == state.MaxCartridges - 1 && state.ComboLastMove == AID.BrutalShell && state.GunComboStep == 0 && state.CD(CDGroup.GnashingFang) < 2.5) return AID.SolidBarrel; if (!Service.Config.Get().Skscheck && state.Ammo == state.MaxCartridges - 1 && state.ComboLastMove == AID.BrutalShell && state.GunComboStep == 0 && state.CD(CDGroup.GnashingFang) < 2.5 && (state.CD(CDGroup.Bloodfest) > 20 && state.Unlocked(AID.Bloodfest))) @@ -313,11 +373,66 @@ 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; + } // single-target gauge spender return GetNextUnlockedComboAction(state, strategy, aoe); } @@ -326,6 +441,7 @@ 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.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 @@ -553,6 +669,9 @@ 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.MaxGaugeBeforeDowntime && state.NoMercyLeft < state.AnimationLock) return ChooseRotationBasedOnGauge(state, strategy, aoe); From 8db81705ea4c44f67b11f15011abb703f7d2afb9 Mon Sep 17 00:00:00 2001 From: Akechiiiii <167795370+Akechiiiii@users.noreply.github.com> Date: Mon, 3 Jun 2024 04:53:19 -0700 Subject: [PATCH 02/10] added Force ST & Force AOE combo added (above) to Gauge, works perfectly under any circumstance in CDPlanner, good for Ultimate use --- BossMod/Autorotation/GNB/GNBRotation.cs | 93 +++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 4 deletions(-) diff --git a/BossMod/Autorotation/GNB/GNBRotation.cs b/BossMod/Autorotation/GNB/GNBRotation.cs index 4ff0cbd08f..9079c488fb 100644 --- a/BossMod/Autorotation/GNB/GNBRotation.cs +++ b/BossMod/Autorotation/GNB/GNBRotation.cs @@ -47,17 +47,23 @@ public enum GaugeUse : uint [PropertyDisplay("Hold Carts", 0x800000ff)] Hold = 2, // Hold carts + [PropertyDisplay("Force ST combo", 0x809061F9)] + ForceST = 3, + + [PropertyDisplay("Force AOE combo", 0x80D1AF97)] + ForceAOE = 4, + [PropertyDisplay("Use Lightning Shot if outside melee", 0x80c08000)] - LightningShotIfNotInMelee = 3, + LightningShotIfNotInMelee = 5, [PropertyDisplay("Use ST combo if still in ST combo, else use AOE combo", 0x80c0c000)] - ComboFitBeforeDowntime = 4, // useful on late phases before downtime + ComboFitBeforeDowntime = 6, // useful on late phases before downtime [PropertyDisplay("Use appropriate rotation to reach max gauge before downtime (NEEDS TESTING)", 0x80c0c000)] - MaxGaugeBeforeDowntime = 5, // useful on late phases before downtime + MaxGaugeBeforeDowntime = 7, // useful on late phases before downtime [PropertyDisplay("Use combo until second-last step, then spend gauge", 0x80400080)] - PenultimateComboThenSpend = 6, // useful for ensuring ST extension is used right before long downtime + PenultimateComboThenSpend = 8, // useful for ensuring ST extension is used right before long downtime } public enum PotionUse : uint @@ -310,6 +316,43 @@ public static AID GetNextAmmoAction(State state, Strategy strategy, bool aoe) 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; + } + } + // todo: Add Hold Double Down only? if (Service.Config.Get().Skscheck && state.Ammo == state.MaxCartridges - 1 && state.ComboLastMove == AID.BrutalShell && state.GunComboStep == 0 && state.CD(CDGroup.GnashingFang) < 2.5) return AID.SolidBarrel; @@ -433,6 +476,40 @@ public static AID GetNextAmmoAction(State state, Strategy strategy, bool aoe) } 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; + } // single-target gauge spender return GetNextUnlockedComboAction(state, strategy, aoe); } @@ -441,6 +518,8 @@ 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.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, @@ -672,6 +751,12 @@ public static AID GetNextBestGCD(State state, Strategy strategy, bool 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.MaxGaugeBeforeDowntime && state.NoMercyLeft < state.AnimationLock) return ChooseRotationBasedOnGauge(state, strategy, aoe); From 379a2785e93bc698231215bf598683ca4938aa66 Mon Sep 17 00:00:00 2001 From: Akechiiiii <167795370+Akechiiiii@users.noreply.github.com> Date: Mon, 3 Jun 2024 05:39:25 -0700 Subject: [PATCH 03/10] added ForceGF combo (GnashingFang) added above to Gauge, meant to Force the full use of Gnashing combo regardless of anything. --- BossMod/Autorotation/GNB/GNBRotation.cs | 76 +++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/BossMod/Autorotation/GNB/GNBRotation.cs b/BossMod/Autorotation/GNB/GNBRotation.cs index 9079c488fb..8e88b5570d 100644 --- a/BossMod/Autorotation/GNB/GNBRotation.cs +++ b/BossMod/Autorotation/GNB/GNBRotation.cs @@ -53,17 +53,20 @@ public enum GaugeUse : uint [PropertyDisplay("Force AOE combo", 0x80D1AF97)] ForceAOE = 4, + [PropertyDisplay("Force Gnashing combo", 0x804C967D)] + ForceGF = 5, + [PropertyDisplay("Use Lightning Shot if outside melee", 0x80c08000)] - LightningShotIfNotInMelee = 5, + LightningShotIfNotInMelee = 6, [PropertyDisplay("Use ST combo if still in ST combo, else use AOE combo", 0x80c0c000)] - ComboFitBeforeDowntime = 6, // 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 = 7, // 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 = 8, // 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 @@ -353,6 +356,36 @@ public static AID GetNextAmmoAction(State state, Strategy strategy, bool aoe) } } + 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; + } + } + } + // todo: Add Hold Double Down only? if (Service.Config.Get().Skscheck && state.Ammo == state.MaxCartridges - 1 && state.ComboLastMove == AID.BrutalShell && state.GunComboStep == 0 && state.CD(CDGroup.GnashingFang) < 2.5) return AID.SolidBarrel; @@ -510,6 +543,37 @@ public static AID GetNextAmmoAction(State state, Strategy strategy, bool aoe) 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 return GetNextUnlockedComboAction(state, strategy, aoe); } @@ -520,6 +584,7 @@ public static AID GetNextAmmoAction(State state, Strategy strategy, bool aoe) 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, @@ -757,6 +822,9 @@ public static AID GetNextBestGCD(State state, Strategy strategy, bool 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); From b53e2a4cc258bb90e664065cfc3b8144544f5477 Mon Sep 17 00:00:00 2001 From: Akechiiiii <167795370+Akechiiiii@users.noreply.github.com> Date: Mon, 3 Jun 2024 06:14:50 -0700 Subject: [PATCH 04/10] Warnings fixes changed "Strategy" prefixes to "CommonRotation.Strategy", fixed warnings --- BossMod/Autorotation/GNB/GNBRotation.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/BossMod/Autorotation/GNB/GNBRotation.cs b/BossMod/Autorotation/GNB/GNBRotation.cs index 8e88b5570d..f38d32711e 100644 --- a/BossMod/Autorotation/GNB/GNBRotation.cs +++ b/BossMod/Autorotation/GNB/GNBRotation.cs @@ -602,11 +602,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; } @@ -642,25 +642,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; } @@ -677,8 +677,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 }; From cbe0d68296c5375ba65e49297b108cd3b2eb6f17 Mon Sep 17 00:00:00 2001 From: Akechi <167795370+Akechiiiii@users.noreply.github.com> Date: Mon, 3 Jun 2024 07:32:22 -0700 Subject: [PATCH 05/10] added Lv70 check for AOE combo there was a bug where GNB AOE would get stuck at Lv70 if No Mercy was active and you had 1+ cartridge, as it would try to cast Fated Circle (Lv72) this is a fix for it, for with or without No Mercy also very minor code cleanup --- BossMod/Autorotation/GNB/GNBRotation.cs | 29 +++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/BossMod/Autorotation/GNB/GNBRotation.cs b/BossMod/Autorotation/GNB/GNBRotation.cs index f38d32711e..8b9d3542cd 100644 --- a/BossMod/Autorotation/GNB/GNBRotation.cs +++ b/BossMod/Autorotation/GNB/GNBRotation.cs @@ -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,27 +34,27 @@ public override string ToString() } // strategy configuration - // TODO: add in Hold Double Down; Force ST or AOE combo + // 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 carts + Hold = 2, // Hold cartridges optimally; works for both ST/AOE [PropertyDisplay("Force ST combo", 0x809061F9)] - ForceST = 3, + ForceST = 3, // forces Single Target combo & protects overcap [PropertyDisplay("Force AOE combo", 0x80D1AF97)] - ForceAOE = 4, + ForceAOE = 4, // forces AOE combo & protects overcap [PropertyDisplay("Force Gnashing combo", 0x804C967D)] - ForceGF = 5, + ForceGF = 5, // forces GF combo [PropertyDisplay("Use Lightning Shot if outside melee", 0x80c08000)] LightningShotIfNotInMelee = 6, @@ -71,7 +71,7 @@ public enum GaugeUse : uint 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, @@ -386,7 +386,6 @@ public static AID GetNextAmmoAction(State state, Strategy strategy, bool aoe) } } - // todo: Add Hold Double Down only? if (Service.Config.Get().Skscheck && state.Ammo == state.MaxCartridges - 1 && state.ComboLastMove == AID.BrutalShell && state.GunComboStep == 0 && state.CD(CDGroup.GnashingFang) < 2.5) return AID.SolidBarrel; if (!Service.Config.Get().Skscheck && state.Ammo == state.MaxCartridges - 1 && state.ComboLastMove == AID.BrutalShell && state.GunComboStep == 0 && state.CD(CDGroup.GnashingFang) < 2.5 && (state.CD(CDGroup.Bloodfest) > 20 && state.Unlocked(AID.Bloodfest))) @@ -417,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) @@ -778,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) @@ -833,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); From 11e3b841cdebec0682ea96d3e64e95b3d1a10746 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Mon, 3 Jun 2024 18:10:10 +0100 Subject: [PATCH 06/10] CS update. --- FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFXIVClientStructs b/FFXIVClientStructs index 44fb68a549..7d68c7440d 160000 --- a/FFXIVClientStructs +++ b/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 44fb68a549f6e2a4b2674a7e9687cd2054de5f36 +Subproject commit 7d68c7440ddb3f42ed63c7acd4a8a7255c1b1e9c From 3d10fc6f81e955474ba3e27a14e1601877f703a9 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Tue, 4 Jun 2024 19:33:41 +0100 Subject: [PATCH 07/10] CS update. --- BossMod/Autorotation/Autorotation.cs | 2 +- BossMod/Debug/DebugObjects.cs | 2 +- BossMod/Debug/DebugParty.cs | 10 ++--- BossMod/Framework/ActionManagerEx.cs | 51 ++++++++++++------------- BossMod/Framework/WorldStateGameSync.cs | 18 ++++----- BossMod/Network/ServerIPC.cs | 4 +- FFXIVClientStructs | 2 +- 7 files changed, 44 insertions(+), 45 deletions(-) diff --git a/BossMod/Autorotation/Autorotation.cs b/BossMod/Autorotation/Autorotation.cs index b72d725660..0b05643694 100644 --- a/BossMod/Autorotation/Autorotation.cs +++ b/BossMod/Autorotation/Autorotation.cs @@ -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/Debug/DebugObjects.cs b/BossMod/Debug/DebugObjects.cs index 13d9be2466..11e3040c88 100644 --- a/BossMod/Debug/DebugObjects.cs +++ b/BossMod/Debug/DebugObjects.cs @@ -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/Framework/ActionManagerEx.cs b/BossMod/Framework/ActionManagerEx.cs index 58b4f98fbb..d2f5f26503 100644 --- a/BossMod/Framework/ActionManagerEx.cs +++ b/BossMod/Framework/ActionManagerEx.cs @@ -1,8 +1,9 @@ -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.System.Framework; +using CSActionType = FFXIVClientStructs.FFXIV.Client.Game.ActionType; namespace BossMod; @@ -77,9 +78,7 @@ 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() { @@ -92,10 +91,7 @@ public ActionManagerEx() _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 +109,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 +168,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 +196,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 +248,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 +273,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,15 +297,15 @@ 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); + var packetAnimLock = header->AnimationLock; + var action = new ActionID((ActionType)header->ActionType, header->ActionId); // note: there's a slight difference with dispatching event from here rather than from packet processing (ActionEffectN) functions // 1. action id is already unscrambled @@ -318,18 +314,19 @@ private void ProcessPacketActionEffectDetour(uint casterID, FFXIVClientStructs.F var info = new ActorCastEvent { Action = action, - MainTargetID = header->animationTargetId, - AnimationLockTime = header->animationLockTime, + 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); @@ -361,9 +358,9 @@ private void ProcessPacketActionEffectDetour(uint casterID, FFXIVClientStructs.F if (adjDelay > AnimationLockDelayMax) { // sanity check for plugin conflicts - if (header->animationLockTime != packetAnimLock || packetAnimLock % 0.01 is >= 0.0005f and <= 0.0095f) + if (header->AnimationLock != 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"); + Service.Log($"[AMEx] Unexpected animation lock {packetAnimLock:f} -> {header->AnimationLock:f}, disabling anim lock tweak feature"); Config.RemoveAnimationLockDelay = false; } else 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/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 7d68c7440d..705119e450 160000 --- a/FFXIVClientStructs +++ b/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 7d68c7440ddb3f42ed63c7acd4a8a7255c1b1e9c +Subproject commit 705119e450a671a76166cec525364414380c5e90 From fc473a8655423f28fe1a5e732afd9e8fc655de08 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Tue, 4 Jun 2024 19:34:12 +0100 Subject: [PATCH 08/10] Fix --- BossMod/Debug/DebugObjects.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BossMod/Debug/DebugObjects.cs b/BossMod/Debug/DebugObjects.cs index 11e3040c88..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)) From b8013c292e3997ee7a3fec69ffc651287e229a82 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Wed, 5 Jun 2024 00:15:46 +0100 Subject: [PATCH 09/10] AMEx refactor: moved animlock tweak to a separate class. --- .../ActionManagerConfig.cs | 3 +- BossMod/ActionTweaks/AnimationLockTweak.cs | 43 +++++++++++ BossMod/Autorotation/Autorotation.cs | 2 +- BossMod/Autorotation/CommonActions.cs | 2 +- BossMod/Framework/ActionManagerEx.cs | 76 ++++++------------- 5 files changed, 71 insertions(+), 55 deletions(-) rename BossMod/{Framework => ActionTweaks}/ActionManagerConfig.cs (94%) create mode 100644 BossMod/ActionTweaks/AnimationLockTweak.cs 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 0b05643694..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); 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/Framework/ActionManagerEx.cs b/BossMod/Framework/ActionManagerEx.cs index d2f5f26503..550299756b 100644 --- a/BossMod/Framework/ActionManagerEx.cs +++ b/BossMod/Framework/ActionManagerEx.cs @@ -2,6 +2,7 @@ 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; @@ -11,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 @@ -54,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 @@ -82,12 +69,7 @@ unsafe sealed class ActionManagerEx : IDisposable 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); @@ -304,16 +286,14 @@ private bool UseBozjaFromHolsterDirectorDetour(PublicContentBozja* self, uint ho private void ProcessPacketActionEffectDetour(uint casterID, Character* casterObj, Vector3* targetPos, ActionEffectHandler.Header* header, ActionEffectHandler.TargetEffects* effects, GameObjectId* targets) { - var packetAnimLock = header->AnimationLock; - var action = new ActionID((ActionType)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, + Action = new ActionID((ActionType)header->ActionType, header->ActionId), MainTargetID = header->AnimationTargetId, AnimationLockTime = header->AnimationLock, MaxTargets = header->NumTargets, @@ -331,46 +311,38 @@ private void ProcessPacketActionEffectDetour(uint casterID, Character* casterObj } 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->AnimationLock != packetAnimLock || packetAnimLock % 0.01 is >= 0.0005f and <= 0.0095f) - { - Service.Log($"[AMEx] Unexpected animation lock {packetAnimLock:f} -> {header->AnimationLock: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) @@ -378,7 +350,7 @@ private void ProcessPacketActionEffectDetour(uint casterID, Character* casterObj 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; } From bf58c28664a32865b03c69570298037e4960b58a Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:42:08 +0200 Subject: [PATCH 10/10] new AI settings --- BossMod/AI/AIBehaviour.cs | 33 +- BossMod/AI/AIConfig.cs | 17 +- BossMod/AI/AIManager.cs | 86 +- BossMod/Components/StackSpread.cs | 3 + BossMod/Config/ConfigUI.cs | 42 +- BossMod/Data/PartyState.cs | 2 + BossMod/Debug/DebugCollision.cs | 812 ++++++++++++++++++ BossMod/Debug/MainDebugWindow.cs | 6 + BossMod/Framework/Plugin.cs | 2 +- BossMod/Framework/Service.cs | 1 + .../Dungeon/D11LapisManalis/D111Albion.cs | 171 ++-- .../Dungeon/D12Aetherfont/D121Lyngbakr.cs | 2 +- .../Dungeon/D13LunarSubterrane/D131DarkElf.cs | 12 - ...amcyanAntilon.cs => D132DamcyanAntlion.cs} | 30 +- .../Dungeon/D13LunarSubterrane/D133Durante.cs | 17 +- .../T04PortaDecumana/T04PortaDecumana1.cs | 15 - .../T04PortaDecumana/T04PortaDecumana2.cs | 16 - .../D01Holminster/D011ForgivenDissonance.cs | 14 - .../D01Holminster/D012TesleentheForgiven.cs | 12 - .../Dungeon/D01Holminster/D013Philia.cs | 27 +- .../Dungeon/D02DohnMheg/D021AencThon.cs | 12 - .../Dungeon/D02DohnMheg/D022Griaule.cs | 12 - .../Dungeon/D02DohnMheg/D023AencThon.cs | 13 - .../D03QitanaRavel/D030RonkanDreamer.cs | 12 - .../Dungeon/D03QitanaRavel/D031Lozatl.cs | 13 - .../Dungeon/D03QitanaRavel/D032Batsquatch.cs | 13 - .../Dungeon/D03QitanaRavel/D033Eros.cs | 15 - .../D04MalikahsWell/D041GreaterArmadillo.cs | 13 - .../D04MalikahsWell/D042AmphibiousTalos.cs | 13 - .../Dungeon/D04MalikahsWell/D043Storge.cs | 12 - .../Dungeon/D05MtGulg/D051ForgivenCruelty.cs | 11 - .../Dungeon/D05MtGulg/D053ForgivenWhimsy.cs | 12 - .../Dungeon/D05MtGulg/D054ForgivenRevelry.cs | 11 - .../D05MtGulg/D055ForgivenObscenity.cs | 12 - .../Dungeon/D06Amaurot/D061TheFirstBeast.cs | 14 - .../Dungeon/D06Amaurot/D062Bellwether.cs | 11 - .../Dungeon/D06Amaurot/D063Therion.cs | 15 +- 37 files changed, 1084 insertions(+), 450 deletions(-) create mode 100644 BossMod/Debug/DebugCollision.cs rename BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/{D132DamcyanAntilon.cs => D132DamcyanAntlion.cs} (86%) 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/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/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/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/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()