diff --git a/BossMod/Autorotation/MiscAI/AutoFarm.cs b/BossMod/Autorotation/MiscAI/AutoFarm.cs index f89432f7c6..95c6b8fa16 100644 --- a/BossMod/Autorotation/MiscAI/AutoFarm.cs +++ b/BossMod/Autorotation/MiscAI/AutoFarm.cs @@ -2,9 +2,10 @@ public sealed class AutoFarm(RotationModuleManager manager, Actor player) : RotationModule(manager, player) { - public enum Track { General, Fate, Specific } + public enum Track { General, Fate, Specific, Mount } public enum GeneralStrategy { FightBack, AllowPull, Aggressive, Passive } public enum PriorityStrategy { None, Prioritize } + public enum MountedStrategy { None, DisableFightBack, DisableAll } public static RotationModuleDefinition Definition() { @@ -24,6 +25,11 @@ public static RotationModuleDefinition Definition() .AddOption(PriorityStrategy.None, "None", "Do not do anything special") .AddOption(PriorityStrategy.Prioritize, "Prioritize", "Prioritize specific mobs by targeting criterion"); + res.Define(Track.Mount).As("Mount") + .AddOption(MountedStrategy.None, "None", "Do not do anything special") + .AddOption(MountedStrategy.DisableFightBack, "NoFightBack", "Do not engage previously uninteresting mobs if they aggro on player") + .AddOption(MountedStrategy.DisableAll, "NoAll", "Do not engage anything while mounted"); + return res; } @@ -34,6 +40,11 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, if (generalStrategy == GeneralStrategy.Passive) return; + var mountStrategy = strategy.Option(Track.Mount).As(); + var mounted = Player.MountId > 0; + if (mounted && mountStrategy == MountedStrategy.DisableAll) + return; + var allowPulling = generalStrategy switch { GeneralStrategy.AllowPull => !Player.InCombat, @@ -58,14 +69,12 @@ void prioritize(AIHints.Enemy e, int prio) // first deal with pulling new enemies if (allowPulling) { - if (Utils.IsPlayerSyncedToFate(World) && strategy.Option(Track.Fate).As() == PriorityStrategy.Prioritize) + var allowFate = Utils.IsPlayerSyncedToFate(World) && strategy.Option(Track.Fate).As() == PriorityStrategy.Prioritize; + foreach (var e in Hints.PotentialTargets) { - foreach (var e in Hints.PotentialTargets) + if (allowFate && e.Actor.FateID == World.Client.ActiveFate.ID && e.Priority == AIHints.Enemy.PriorityUndesirable) { - if (e.Actor.FateID == World.Client.ActiveFate.ID && e.Priority == AIHints.Enemy.PriorityUndesirable) - { - prioritize(e, 1); - } + prioritize(e, 1); } } @@ -85,7 +94,9 @@ void prioritize(AIHints.Enemy e, int prio) } // if we did not select an enemy to pull, see if we can target something higher-priority than what we have now - if (switchTarget == null && Player.InCombat) + // if mounted, check if the "fight back" strategy is undesired + var mountNoFightBack = mounted && mountStrategy == MountedStrategy.DisableFightBack; + if (switchTarget == null && Player.InCombat && !mountNoFightBack) { var curTargetPrio = Hints.FindEnemy(primaryTarget)?.Priority ?? int.MinValue; switchTarget = ResolveTargetOverride(generalOpt.Value) ?? (curTargetPrio < Hints.HighestPotentialTargetPriority ? Hints.PriorityTargets.MinBy(e => (e.Actor.Position - Player.Position).LengthSq())?.Actor : null); diff --git a/BossMod/Autorotation/MiscAI/AutoPull.cs b/BossMod/Autorotation/MiscAI/AutoPull.cs index db0ebf8c24..e93f88ad8b 100644 --- a/BossMod/Autorotation/MiscAI/AutoPull.cs +++ b/BossMod/Autorotation/MiscAI/AutoPull.cs @@ -8,7 +8,7 @@ public enum Track { QuestBattle, DeepDungeon, EpicEcho, Hunt } public static RotationModuleDefinition Definition() { - var def = new RotationModuleDefinition("Misc AI: Auto-pull", "Automatically attack passive mobs in certain circumstances", "Misc", "xan", RotationModuleQuality.Basic, new(~0ul), 1000, Order: RotationModuleOrder.HighLevel, CanUseWhileRoleplaying: true); + var def = new RotationModuleDefinition("Auto-pull", "Automatically attack passive mobs in certain circumstances", "AI", "xan", RotationModuleQuality.Basic, new(~0ul), 1000, Order: RotationModuleOrder.HighLevel, CanUseWhileRoleplaying: true); def.AbilityTrack(Track.QuestBattle, "Automatically attack solo duty bosses"); def.AbilityTrack(Track.DeepDungeon, "Automatically attack deep dungeon bosses when solo"); diff --git a/BossMod/Autorotation/RotationModule.cs b/BossMod/Autorotation/RotationModule.cs index bf884f5b48..9e40d69fda 100644 --- a/BossMod/Autorotation/RotationModule.cs +++ b/BossMod/Autorotation/RotationModule.cs @@ -170,9 +170,14 @@ public int FindDutyActionSlot(ActionID action, ActionID other) protected (Actor? Target, P Priority) FindBetterTargetBy

(Actor? initial, float maxDistanceFromPlayer, Func prioFunc, Func? filterFunc = null) where P : struct, IComparable { + bool inRange(Actor tar) => tar.Position.InCircle(Player.Position, maxDistanceFromPlayer + tar.HitboxRadius + 0.5f); + + if (initial != null && !inRange(initial)) + initial = null; + var bestTarget = initial; var bestPrio = initial != null ? prioFunc(initial) : default; - foreach (var enemy in Hints.PriorityTargets.Where(x => x.Actor != initial && x.Actor.Position.InCircle(Player.Position, maxDistanceFromPlayer + x.Actor.HitboxRadius) && (filterFunc?.Invoke(x) ?? true))) + foreach (var enemy in Hints.PriorityTargets.Where(x => x.Actor != initial && inRange(x.Actor) && (filterFunc?.Invoke(x) ?? true))) { var newPrio = prioFunc(enemy.Actor); if (newPrio.CompareTo(bestPrio) > 0) diff --git a/BossMod/Autorotation/Standard/xan/AI/Healer.cs b/BossMod/Autorotation/Standard/xan/AI/Healer.cs index a688bea0da..183bf162eb 100644 --- a/BossMod/Autorotation/Standard/xan/AI/Healer.cs +++ b/BossMod/Autorotation/Standard/xan/AI/Healer.cs @@ -9,6 +9,12 @@ public class HealerAI(RotationModuleManager manager, Actor player) : AIBase(mana private readonly TrackPartyHealth Health = new(manager.WorldState); public enum Track { Raise, RaiseTarget, Heal, Esuna, StayNearParty } + public enum EsunaStrategy + { + None, + Hinted, + All + } public enum RaiseStrategy { None, @@ -48,7 +54,12 @@ public static RotationModuleDefinition Definition() .AddOption(RaiseTarget.Everyone, "Any dead player"); def.AbilityTrack(Track.Heal, "Heal"); - def.AbilityTrack(Track.Esuna, "Esuna"); + + def.Define(Track.Esuna).As("Esuna") + .AddOption(EsunaStrategy.None, "Don't cleanse") + .AddOption(EsunaStrategy.Hinted, "Cleanse targets suggested by active module") + .AddOption(EsunaStrategy.All, "Cleanse all party members that have a removable debuff"); + def.AbilityTrack(Track.StayNearParty, "Stay near party"); return def; @@ -97,11 +108,13 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, AutoRaise(strategy); - if (strategy.Enabled(Track.Esuna)) + var esuna = strategy.Option(Track.Esuna).As(); + + if (esuna != EsunaStrategy.None) { foreach (var st in Health.PartyMemberStates) { - if (st.EsunableStatusRemaining > GCD + 2f) + if (st.EsunableStatusRemaining > GCD + 1.14f && (esuna == EsunaStrategy.All || Hints.ShouldCleanse[st.Slot])) { UseGCD(BossMod.WHM.AID.Esuna, World.Party[st.Slot]); break; diff --git a/BossMod/Autorotation/Standard/xan/AI/Tank.cs b/BossMod/Autorotation/Standard/xan/AI/Tank.cs index 5d26a0037f..e9ac6e5262 100644 --- a/BossMod/Autorotation/Standard/xan/AI/Tank.cs +++ b/BossMod/Autorotation/Standard/xan/AI/Tank.cs @@ -202,21 +202,21 @@ private void AutoProtect() private void AutoMit() { - if (EnemiesAutoingMe.Any()) - { - if (Player.PredictedHPRatio < 0.8) - { - var delay = 0f; - if (JobActions.ShortMit.ID == ActionID.MakeSpell(WAR.AID.RawIntuition)) - delay = GCD - 0.8f; - Hints.ActionsToExecute.Push(JobActions.ShortMit.ID, Player, ActionQueue.Priority.Minimal, delay: delay); - } + if (Player.PredictedHPRaw == Player.HPMP.CurHP && !Player.InCombat) + return; - if (Player.PredictedHPRatio < 0.6) - // set arbitrary deadline to 1 second in the future - UseOneMit(1); + if (Player.PredictedHPRatio < 0.8) + { + var delay = 0f; + if (JobActions.ShortMit.ID == ActionID.MakeSpell(WAR.AID.RawIntuition)) + delay = GCD - 0.8f; + Hints.ActionsToExecute.Push(JobActions.ShortMit.ID, Player, ActionQueue.Priority.Minimal, delay: delay); } + if (Player.PredictedHPRatio < 0.6) + // set arbitrary deadline to 1 second in the future + UseOneMit(1); + // TODO figure out how consistent this is or if we should use predictively instead if (Player.PredictedHPRaw <= 0) Hints.ActionsToExecute.Push(JobActions.Invuln.ID, Player, ActionQueue.Priority.VeryHigh); diff --git a/BossMod/Autorotation/Standard/xan/Basexan.cs b/BossMod/Autorotation/Standard/xan/Basexan.cs index 6f9b833dfe..ec2f9bfe7a 100644 --- a/BossMod/Autorotation/Standard/xan/Basexan.cs +++ b/BossMod/Autorotation/Standard/xan/Basexan.cs @@ -215,7 +215,7 @@ P targetPrio(Actor potentialTarget) targeting = Targeting.Manual; if (targeting == Targeting.AutoTryPri) - targeting = primaryTarget == null ? Targeting.Auto : Targeting.AutoPrimary; + targeting = Player.DistanceToHitbox(primaryTarget) <= range ? Targeting.AutoPrimary : Targeting.Auto; var (newtarget, newprio) = targeting switch { @@ -288,8 +288,10 @@ protected void GoalZoneSingle(float range) Hints.GoalZones.Add(Hints.GoalSingleTarget(PlayerTarget.Actor, range)); } - protected void GoalZoneCombined(StrategyValues strategy, float range, Func fAoe, AID firstUnlockedAoeAction, int minAoe, Positional positional = Positional.Any, float? maximumActionRange = null) + protected void GoalZoneCombined(StrategyValues strategy, float range, Func fAoe, AID firstUnlockedAoeAction, int minAoe, float? maximumActionRange = null) { + var (_, positional, imminent, _) = Hints.RecommendedPositional; + if (!strategy.AOEOk() || !Unlocked(firstUnlockedAoeAction)) minAoe = 50; @@ -300,7 +302,7 @@ protected void GoalZoneCombined(StrategyValues strategy, float range, Func GCD; var target = enemy?.Actor; if ((target?.Omnidirectional ?? true) || target?.TargetID == Player.InstanceID && target?.CastInfo == null && positional.pos != Positional.Front && target?.NameID != 541) positional = (Positional.Any, false); diff --git a/BossMod/Autorotation/Standard/xan/Casters/RDM.cs b/BossMod/Autorotation/Standard/xan/Casters/RDM.cs index 6e6afc4dcb..6c3983493a 100644 --- a/BossMod/Autorotation/Standard/xan/Casters/RDM.cs +++ b/BossMod/Autorotation/Standard/xan/Casters/RDM.cs @@ -220,7 +220,8 @@ private void OGCD(StrategyValues strategy, Enemy? primaryTarget) if (strategy.BuffsOk()) { PushOGCD(AID.Embolden, Player); - PushOGCD(AID.Manafication, Player); + if (!InCombo) + PushOGCD(AID.Manafication, Player); } PushOGCD(AID.ContreSixte, BestAOETarget); diff --git a/BossMod/Autorotation/Standard/xan/Healers/WHM.cs b/BossMod/Autorotation/Standard/xan/Healers/WHM.cs index ef145907e6..7852c18da5 100644 --- a/BossMod/Autorotation/Standard/xan/Healers/WHM.cs +++ b/BossMod/Autorotation/Standard/xan/Healers/WHM.cs @@ -89,7 +89,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) PushGCD(AID.Holy1, Player); // TODO make a track for this - if (Lily == 3 || !CanFitGCD(NextLily, 2) && Lily == 2) + if (Unlocked(AID.AfflatusMisery) && (Lily == 3 || !CanFitGCD(NextLily, 2) && Lily == 2)) PushGCD(AID.AfflatusSolace, Player); if (SacredSight > 0) diff --git a/BossMod/Autorotation/Standard/xan/Melee/DRG.cs b/BossMod/Autorotation/Standard/xan/Melee/DRG.cs index 0b1d90bdd9..09214ddf4a 100644 --- a/BossMod/Autorotation/Standard/xan/Melee/DRG.cs +++ b/BossMod/Autorotation/Standard/xan/Melee/DRG.cs @@ -81,7 +81,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) (BestDiveTarget, NumDiveTargets) = SelectTarget(strategy, primaryTarget, 20, IsSplashTarget); var pos = GetPositional(strategy, primaryTarget); - UpdatePositionals(primaryTarget, ref pos, TrueNorthLeft > GCD); + UpdatePositionals(primaryTarget, ref pos); if (primaryTarget == null) return; @@ -94,7 +94,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) return; } - GoalZoneCombined(strategy, 3, Hints.GoalAOERect(primaryTarget.Actor, 10, 2), AID.DoomSpike, minAoe: 3, positional: pos.Item1, maximumActionRange: 20); + GoalZoneCombined(strategy, 3, Hints.GoalAOERect(primaryTarget.Actor, 10, 2), AID.DoomSpike, minAoe: 3, maximumActionRange: 20); if (NumAOETargets > 2) { diff --git a/BossMod/Autorotation/Standard/xan/Melee/MNK.cs b/BossMod/Autorotation/Standard/xan/Melee/MNK.cs index 8e2927474e..0fbd2d95e8 100644 --- a/BossMod/Autorotation/Standard/xan/Melee/MNK.cs +++ b/BossMod/Autorotation/Standard/xan/Melee/MNK.cs @@ -6,7 +6,7 @@ namespace BossMod.Autorotation.xan; public sealed class MNK(RotationModuleManager manager, Actor player) : Attackxan(manager, player) { - public enum Track { BH = SharedTrack.Buffs, RoF, FiresReply, RoW, WindsReply, PB, Nadi, Blitz, SSS, FormShift, Meditation, TC, Potion, Engage, TN, Positional } + public enum Track { BH = SharedTrack.Buffs, RoF, FiresReply, RoW, WindsReply, PB, Nadi, Blitz, SSS, FormShift, Meditation, TC, Potion, Engage, TN } public enum PotionStrategy { Manual, @@ -118,7 +118,6 @@ public static RotationModuleDefinition Definition() .AddOption(WRStrategy.PreDowntime, "Ensure usage at least 2 GCDs before next downtime", minLevel: 96) .AddAssociatedActions(AID.WindsReply); - // PB-related settings def.Define(Track.PB).As("PB", uiPriority: 89) .AddOption(PBStrategy.Automatic, "Automatically use after Opo before or during Riddle of Fire", minLevel: 50) @@ -159,7 +158,8 @@ public static RotationModuleDefinition Definition() def.Define(Track.Potion).As("Pot", uiPriority: 59) .AddOption(PotionStrategy.Manual, "Do not automatically use") .AddOption(PotionStrategy.PreBuffs, "Use ~4 GCDs before raid buff window") - .AddOption(PotionStrategy.Now, "Use ASAP"); + .AddOption(PotionStrategy.Now, "Use ASAP") + .AddAssociatedAction(ActionDefinitions.IDPotionStr); def.Define(Track.Engage).As("Engage", uiPriority: 49) .AddOption(EngageStrategy.TC, "Thunderclap to target") @@ -168,10 +168,6 @@ public static RotationModuleDefinition Definition() .AddOption(EngageStrategy.FacepullDemo, "Precast Demolish from melee range"); def.DefineSimple(Track.TN, "TrueNorth", minLevel: 50, uiPriority: 48).AddAssociatedActions(AID.TrueNorth); - def.Define(Track.Positional).As("Pos (AI)", uiPriority: 45) - .AddOption(PositionalStrategy.Automatic, "Tell AI mode to navigate to hit positionals") - .AddOption(PositionalStrategy.Ignore, "Tell AI mode to ignore positionals") - .AddAssociatedActions(AID.Demolish, AID.SnapPunch, AID.PouncingCoeurl); return def; } @@ -410,11 +406,11 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) Prep(strategy); - var pos = strategy.Option(Track.Positional).As() == PositionalStrategy.Automatic ? NextPositional : (Positional.Any, false); + var pos = NextPositional; - UpdatePositionals(primaryTarget, ref pos, TrueNorthLeft > GCD); + UpdatePositionals(primaryTarget, ref pos); - GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.ArmOfTheDestroyer, AOEBreakpoint, positional: pos.Item1, maximumActionRange: 20); + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.ArmOfTheDestroyer, AOEBreakpoint, maximumActionRange: 20); if (Player.InCombat) OGCD(strategy, primaryTarget); @@ -548,14 +544,16 @@ private void QueuePB(StrategyValues strategy, Enemy? primaryTarget) private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { - switch (strategy.Option(Track.Potion).As()) + var potionTrack = strategy.Option(Track.Potion); + var potionPrio = potionTrack.Priority(ActionQueue.Priority.Low + 100 + (float)OGCDPriority.Potion); + switch (potionTrack.As()) { case PotionStrategy.Now: - Potion(); + Potion(potionPrio); break; case PotionStrategy.PreBuffs: if (HaveTarget && CanWeave(AID.Brotherhood, 4)) - Potion(); + Potion(potionPrio); break; } @@ -718,7 +716,7 @@ private void WindsReply(StrategyValues strategy) private float DesiredFireWindow => GCDLength * 10; private float EarliestRoF(float estimatedDelay) => MathF.Max(estimatedDelay + 0.8f, 20.6f - DesiredFireWindow); - private void Potion() => Hints.ActionsToExecute.Push(ActionDefinitions.IDPotionStr, Player, ActionQueue.Priority.Low + 100 + (float)OGCDPriority.Potion); + private void Potion(float priority) => Hints.ActionsToExecute.Push(ActionDefinitions.IDPotionStr, Player, priority); private (bool Use, bool LateWeave) ShouldRoF(StrategyValues strategy, int extraGCDs = 0) { diff --git a/BossMod/Autorotation/Standard/xan/Melee/NIN.cs b/BossMod/Autorotation/Standard/xan/Melee/NIN.cs index 41f8586d0c..26c72c729b 100644 --- a/BossMod/Autorotation/Standard/xan/Melee/NIN.cs +++ b/BossMod/Autorotation/Standard/xan/Melee/NIN.cs @@ -121,7 +121,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) NumAOETargets = NumMeleeAOETargets(strategy); var pos = GetNextPositional(primaryTarget?.Actor); - UpdatePositionals(primaryTarget, ref pos, TrueNorthLeft > GCD); + UpdatePositionals(primaryTarget, ref pos); OGCD(strategy, primaryTarget); @@ -133,7 +133,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) return; } - GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.DeathBlossom, minAoe: 3, positional: pos.Item1, maximumActionRange: 20); + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.DeathBlossom, minAoe: 3, maximumActionRange: 20); if (TenChiJin.Left > GCD) { diff --git a/BossMod/Autorotation/Standard/xan/Melee/RPR.cs b/BossMod/Autorotation/Standard/xan/Melee/RPR.cs index c6209c0183..08c5283fa3 100644 --- a/BossMod/Autorotation/Standard/xan/Melee/RPR.cs +++ b/BossMod/Autorotation/Standard/xan/Melee/RPR.cs @@ -124,7 +124,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) (BestRangedAOETarget, NumRangedAOETargets) = SelectTarget(strategy, primaryTarget, 25, IsSplashTarget); var pos = GetNextPositional(primaryTarget?.Actor); - UpdatePositionals(primaryTarget, ref pos, TrueNorthLeft > GCD); + UpdatePositionals(primaryTarget, ref pos); OGCD(strategy, primaryTarget); @@ -136,7 +136,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) return; } - GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.SpinningScythe, 3, pos.Item1, maximumActionRange: 25); + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.SpinningScythe, 3, maximumActionRange: 25); if (SoulReaver > GCD || Executioner > GCD) { diff --git a/BossMod/Autorotation/Standard/xan/Melee/SAM.cs b/BossMod/Autorotation/Standard/xan/Melee/SAM.cs index d23d860b46..abd43812c1 100644 --- a/BossMod/Autorotation/Standard/xan/Melee/SAM.cs +++ b/BossMod/Autorotation/Standard/xan/Melee/SAM.cs @@ -153,7 +153,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) } var pos = GetNextPositional(strategy); - UpdatePositionals(primaryTarget, ref pos, TrueNorthLeft > GCD); + UpdatePositionals(primaryTarget, ref pos); OGCD(strategy, primaryTarget); @@ -171,7 +171,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) return; } - GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(NumStickers == 2 ? 8 : 5), AID.Fuga, 3, pos.Item1, 20); + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(NumStickers == 2 ? 8 : 5), AID.Fuga, 3, 20); EmergencyMeikyo(strategy, primaryTarget); UseKaeshi(primaryTarget); diff --git a/BossMod/Autorotation/Standard/xan/Melee/VPR.cs b/BossMod/Autorotation/Standard/xan/Melee/VPR.cs index eef5107de1..b871333283 100644 --- a/BossMod/Autorotation/Standard/xan/Melee/VPR.cs +++ b/BossMod/Autorotation/Standard/xan/Melee/VPR.cs @@ -113,7 +113,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) NumAOETargets = NumMeleeAOETargets(strategy); var pos = GetPositional(strategy); - UpdatePositionals(primaryTarget, ref pos, TrueNorthLeft > GCD); + UpdatePositionals(primaryTarget, ref pos); OGCD(strategy, primaryTarget); @@ -127,7 +127,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) _ => Anguine > 0 ? 50 : 3 }; - GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.SteelMaw, aoeBreakpoint, pos.Item1, 20); + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.SteelMaw, aoeBreakpoint, 20); if (CombatTimer < 0.5f && Player.DistanceToHitbox(primaryTarget) > 3) PushGCD(AID.Slither, primaryTarget); diff --git a/BossMod/Autorotation/UIPresetEditor.cs b/BossMod/Autorotation/UIPresetEditor.cs index abe193466b..ff5e021284 100644 --- a/BossMod/Autorotation/UIPresetEditor.cs +++ b/BossMod/Autorotation/UIPresetEditor.cs @@ -344,6 +344,9 @@ private bool DrawModifier(ref Preset.Modifier mod, Preset.Modifier flag, string private bool CheckNameConflict() { + if (_db.DefaultPresets.Any(p => p.Name == Preset.Name)) + return true; + for (int i = 0; i < _db.UserPresets.Count; ++i) if (i != _sourcePresetIndex && _db.UserPresets[i].Name == Preset.Name) return true; diff --git a/BossMod/BossModule/AIHints.cs b/BossMod/BossModule/AIHints.cs index 180828d3a2..1b9e723a0a 100644 --- a/BossMod/BossModule/AIHints.cs +++ b/BossMod/BossModule/AIHints.cs @@ -86,6 +86,9 @@ public enum SpecialMode // AI will attempt to shield & mitigate public List<(BitMask players, DateTime activation)> PredictedDamage = []; + // list of party members with cleansable debuffs that are dangerous enough to sacrifice a GCD to cleanse them, i.e. doom, throttle, some types of vuln debuff, etc + public BitMask ShouldCleanse; + // maximal time we can spend casting before we need to move // this is used by the action queue to skip casts that we won't be able to finish and execute lower-priority fallback actions instead public float MaxCastTime = float.MaxValue; @@ -119,6 +122,7 @@ public void Clear() ImminentSpecialMode = default; MisdirectionThreshold = 15.Degrees(); PredictedDamage.Clear(); + ShouldCleanse.Reset(); MaxCastTime = float.MaxValue; ForceCancelCast = false; ActionsToExecute.Clear(); @@ -312,4 +316,21 @@ public Func GoalProximity(WPos destination, float maxDistance, floa return maxWeight * weight; }; } + + public Func PullTargetToLocation(Actor target, WPos destination, float destRadius = 2) + { + var enemy = FindEnemy(target); + if (enemy == null) + return _ => 0; + + var adjRange = enemy.TankDistance + target.HitboxRadius + 0.5f; + var desiredToTarget = target.Position - destination; + var leewaySq = destRadius * destRadius; + if (desiredToTarget.LengthSq() > leewaySq) + { + var dest = destination - adjRange * desiredToTarget.Normalized(); + return GoalSingleTarget(dest, PathfindMapBounds.MapResolution, 10); + } + return _ => 0; + } } diff --git a/BossMod/BossModule/BossModule.cs b/BossMod/BossModule/BossModule.cs index b297284ef1..d35d8d636d 100644 --- a/BossMod/BossModule/BossModule.cs +++ b/BossMod/BossModule/BossModule.cs @@ -14,6 +14,7 @@ public abstract class BossModule : IDisposable public readonly MiniArena Arena; public readonly BossModuleRegistry.Info? Info; public readonly StateMachine StateMachine; + public readonly Pathfinding.ObstacleMapManager Obstacles; private readonly EventSubscriptions _subscriptions; @@ -79,6 +80,7 @@ public void DeactivateComponent() where T : BossComponent protected BossModule(WorldState ws, Actor primary, WPos center, ArenaBounds bounds) { + Obstacles = new(ws); WorldState = ws; PrimaryActor = primary; Arena = new(WindowConfig, center, bounds); @@ -122,6 +124,7 @@ protected virtual void Dispose(bool disposing) ClearComponents(_ => true); _subscriptions.Dispose(); + Obstacles.Dispose(); } public void Update() @@ -222,6 +225,17 @@ public void CalculateAIHints(int slot, Actor actor, PartyRolesConfig.Assignment { hints.PathfindMapCenter = Center; hints.PathfindMapBounds = Bounds; + + var (entry, bitmap) = Obstacles.Find(new Vector3(Center.X, actor.PosRot.Y, Center.Z)); + if (entry != null && bitmap != null) + { + var originCell = (Center - entry.Origin) / bitmap.PixelSize; + var originX = (int)originCell.X; + var originZ = (int)originCell.Z; + var halfSize = (int)(Bounds.Radius / bitmap.PixelSize); + hints.PathfindMapObstacles = new(bitmap, new(originX - halfSize, originZ - halfSize, originX + halfSize, originZ + halfSize)); + } + foreach (var comp in _components) comp.AddAIHints(slot, actor, assignment, hints); CalculateModuleAIHints(slot, actor, assignment, hints); diff --git a/BossMod/BossModule/BossModuleInfo.cs b/BossMod/BossModule/BossModuleInfo.cs index 515baae890..e04fe852a3 100644 --- a/BossMod/BossModule/BossModuleInfo.cs +++ b/BossMod/BossModule/BossModuleInfo.cs @@ -64,6 +64,7 @@ public enum GroupType Hunt, // group id is HuntRank BozjaCE, // group id is ContentFinderCondition row, name id is DynamicEvent row BozjaDuel, // group id is ContentFinderCondition row, name id is DynamicEvent row + EurekaNM, // group id is ContentFinderCondition row, name id is Fate row GoldSaucer, // group id is GoldSaucerTextData row } diff --git a/BossMod/Components/Adds.cs b/BossMod/Components/Adds.cs index d7692bc459..a0f22f4900 100644 --- a/BossMod/Components/Adds.cs +++ b/BossMod/Components/Adds.cs @@ -18,6 +18,16 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) } } +// component for adds that shouldn't be targeted at all, but should still be drawn +public class AddsPointless(BossModule module, uint oid) : Adds(module, oid) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var act in ActiveActors) + hints.SetPriority(act, AIHints.Enemy.PriorityPointless); + } +} + // generic component used for drawing multiple adds with multiple oids, when it's not useful to distinguish between them public class AddsMulti(BossModule module, uint[] oids, int priority = 0) : BossComponent(module) { diff --git a/BossMod/Config/ModuleViewer.cs b/BossMod/Config/ModuleViewer.cs index d42cbbeb24..542724ff4c 100644 --- a/BossMod/Config/ModuleViewer.cs +++ b/BossMod/Config/ModuleViewer.cs @@ -3,7 +3,6 @@ using Dalamud.Interface.Utility.Raii; using ImGuiNET; using Lumina.Excel.Sheets; -using Lumina.Text; using Lumina.Text.ReadOnly; using System.Data; using System.Globalization; @@ -287,6 +286,10 @@ private void DrawModules(UITree tree, WorldState ws) groupId |= module.GroupID; var duelName = $"{FixCase(Service.LuminaRow(module.GroupID)!.Value.Name)} Duel"; return (new(duelName, groupId, groupId), new(module, Service.LuminaRow(module.NameID)!.Value.Name.ToString(), module.SortOrder)); + case BossModuleInfo.GroupType.EurekaNM: + groupId |= module.GroupID; + var nmName = FixCase(Service.LuminaRow(module.GroupID)!.Value.Name); + return (new(nmName, groupId, groupId), new(module, Service.LuminaRow(module.NameID)!.Value.Name.ToString(), module.SortOrder)); case BossModuleInfo.GroupType.GoldSaucer: return (new("Gold saucer", groupId, groupId), new(module, $"{Service.LuminaRow(module.GroupID)?.Text}: {BNpcName(module.NameID)}", module.SortOrder)); default: diff --git a/BossMod/Data/Actor.cs b/BossMod/Data/Actor.cs index 2e63cf7ddf..765651add6 100644 --- a/BossMod/Data/Actor.cs +++ b/BossMod/Data/Actor.cs @@ -61,7 +61,7 @@ public sealed record class ActorCastEvent(ActionID Action, ulong MainTargetID, f public bool IsSpell(AID aid) where AID : Enum => Action == ActionID.MakeSpell(aid); } -public record struct ActorHPMP(uint CurHP, uint MaxHP, uint Shield, uint CurMP); +public record struct ActorHPMP(uint CurHP, uint MaxHP, uint Shield, uint CurMP, uint MaxMP); // note on tethers - it is N:1 type of relation, actor can be tethered to 0 or 1 actors, but can itself have multiple actors tethering themselves to itself // target is an instance id @@ -71,6 +71,8 @@ public record struct ActorStatus(uint ID, ushort Extra, DateTime ExpireAt, ulong public record struct ActorModelState(byte ModelState, byte AnimState1, byte AnimState2); +public record struct ActorForayInfo(byte Level, byte Element); + public readonly record struct ActorIncomingEffect(uint GlobalSequence, int TargetIndex, ulong SourceInstanceId, ActionID Action, ActionEffects Effects); public record struct PendingEffect(uint GlobalSequence, int TargetIndex, ulong SourceInstanceId, DateTime Expiration); public record struct PendingEffectDelta(PendingEffect Effect, int Value); @@ -99,6 +101,7 @@ public sealed class Actor(ulong instanceID, uint oid, int spawnIndex, string nam public bool InCombat; public bool AggroPlayer; // determines whether a given actor shows in the player's UI enemy list public ActorModelState ModelState; + public ActorForayInfo ForayInfo; public byte EventState; // not sure about the field meaning... public ulong OwnerID = ownerID; // uuid of owner, for pets and similar public ulong TargetID; diff --git a/BossMod/Data/ActorState.cs b/BossMod/Data/ActorState.cs index 2688c7c891..1a4c7fe8c3 100644 --- a/BossMod/Data/ActorState.cs +++ b/BossMod/Data/ActorState.cs @@ -36,6 +36,8 @@ public IEnumerable CompareToInitial() yield return new OpTarget(act.InstanceID, act.TargetID); if (act.MountId != 0) yield return new OpMount(act.InstanceID, act.MountId); + if (act.ForayInfo != default) + yield return new OpForayInfo(act.InstanceID, act.ForayInfo); if (act.Tether.ID != 0) yield return new OpTether(act.InstanceID, act.Tether); if (act.CastInfo != null) @@ -146,6 +148,7 @@ public override void Write(ReplayRecorder.Output output) => output.EmitFourCC("A .Emit(HPMP.MaxHP) .Emit(HPMP.Shield) .Emit(HPMP.CurMP) + .Emit(HPMP.MaxMP) .Emit(IsTargetable) .Emit(IsAlly) .EmitActor(OwnerID) @@ -241,7 +244,7 @@ protected override void ExecActor(WorldState ws, Actor actor) actor.HPMP = HPMP; ws.Actors.HPMPChanged.Fire(actor); } - public override void Write(ReplayRecorder.Output output) => output.EmitFourCC("HP "u8).EmitActor(InstanceID).Emit(HPMP.CurHP).Emit(HPMP.MaxHP).Emit(HPMP.Shield).Emit(HPMP.CurMP); + public override void Write(ReplayRecorder.Output output) => output.EmitFourCC("HP "u8).EmitActor(InstanceID).Emit(HPMP.CurHP).Emit(HPMP.MaxHP).Emit(HPMP.Shield).Emit(HPMP.CurMP).Emit(HPMP.MaxMP); } public Event IsTargetableChanged = new(); @@ -343,6 +346,17 @@ protected override void ExecActor(WorldState ws, Actor actor) public override void Write(ReplayRecorder.Output output) => output.EmitFourCC("MNTD"u8).EmitActor(InstanceID).Emit(Value); } + public Event ForayInfoChanged = new(); + public sealed record class OpForayInfo(ulong InstanceID, ActorForayInfo Value) : Operation(InstanceID) + { + protected override void ExecActor(WorldState ws, Actor actor) + { + actor.ForayInfo = Value; + ws.Actors.ForayInfoChanged.Fire(actor); + } + public override void Write(ReplayRecorder.Output output) => output.EmitFourCC("FORA"u8).EmitActor(InstanceID).Emit(Value.Level).Emit(Value.Element); + } + // note: this is currently based on network events rather than per-frame state inspection public Event Tethered = new(); public Event Untethered = new(); // note that actor structure still contains previous tether info when this is invoked; invoked if actor disappears without untethering diff --git a/BossMod/Data/ClientState.cs b/BossMod/Data/ClientState.cs index b33d14ba6d..32806eddfd 100644 --- a/BossMod/Data/ClientState.cs +++ b/BossMod/Data/ClientState.cs @@ -385,4 +385,11 @@ public override void Write(ReplayRecorder.Output output) output.Emit(val); } } + + public Event FateInfo = new(); + public sealed record class OpFateInfo(uint FateId, DateTime StartTime) : WorldState.Operation + { + protected override void Exec(WorldState ws) => ws.Client.FateInfo.Fire(this); + public override void Write(ReplayRecorder.Output output) => output.EmitFourCC("FATE"u8).Emit(FateId).Emit(StartTime.Ticks); + } } diff --git a/BossMod/Framework/MovementOverride.cs b/BossMod/Framework/MovementOverride.cs index a76ed0016b..063f476249 100644 --- a/BossMod/Framework/MovementOverride.cs +++ b/BossMod/Framework/MovementOverride.cs @@ -1,4 +1,5 @@ using Dalamud.Game.Config; +using Dalamud.Plugin; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Input; @@ -28,10 +29,12 @@ public sealed unsafe class MovementOverride : IDisposable public WDir UserMove { get; private set; } // unfiltered movement direction, as read from input public WDir ActualMove { get; private set; } // actual movement direction, as of last input read + private readonly IDalamudPluginInterface _dalamud; private readonly ActionTweaksConfig _tweaksConfig = Service.Config.Get(); private bool _movementBlocked; private bool? _forcedControlState; private bool _legacyMode; + private bool[]? _navmeshPathIsRunning; public bool IsMoving() => ActualMove != default; public bool IsMoveRequested() => UserMove != default; @@ -67,8 +70,10 @@ public bool MovementBlocked private delegate byte MoveControlIsInputActiveDelegate(void* self, byte inputSourceFlags); private readonly HookAddress _mcIsInputActiveHook; - public MovementOverride() + public MovementOverride(IDalamudPluginInterface dalamud) { + _dalamud = dalamud; + var rmiWalkIsInputEnabled1Addr = Service.SigScanner.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 38 43 3C"); var rmiWalkIsInputEnabled2Addr = Service.SigScanner.ScanText("E8 ?? ?? ?? ?? 84 C0 75 03 88 47 3F"); Service.Log($"RMIWalkIsInputEnabled1 address: 0x{rmiWalkIsInputEnabled1Addr:X}"); @@ -86,6 +91,7 @@ public MovementOverride() public void Dispose() { + _dalamud.RelinquishData("vnav.PathIsRunning"); Service.GameConfig.UiControlChanged -= OnConfigChanged; _movementBlocked = false; _mcIsInputActiveHook.Dispose(); @@ -93,13 +99,19 @@ public void Dispose() _rmiFlyHook.Dispose(); } + private bool NavmeshActive() + { + _navmeshPathIsRunning ??= _dalamud.GetData("vnav.PathIsRunning"); + return _navmeshPathIsRunning != null && _navmeshPathIsRunning[0]; + } + private void RMIWalkDetour(void* self, float* sumLeft, float* sumForward, float* sumTurnLeft, byte* haveBackwardOrStrafe, byte* a6, byte bAdditiveUnk) { _forcedControlState = null; _rmiWalkHook.Original(self, sumLeft, sumForward, sumTurnLeft, haveBackwardOrStrafe, a6, bAdditiveUnk); // TODO: we really need to introduce some extra checks that PlayerMoveController::readInput does - sometimes it skips reading input, and returning something non-zero breaks stuff... - var movementAllowed = bAdditiveUnk == 0 && _rmiWalkIsInputEnabled1(self) && _rmiWalkIsInputEnabled2(self); + var movementAllowed = bAdditiveUnk == 0 && _rmiWalkIsInputEnabled1(self) && _rmiWalkIsInputEnabled2(self) && !NavmeshActive(); var misdirectionMode = PlayerHasMisdirection(); if (!movementAllowed && misdirectionMode) { diff --git a/BossMod/Framework/Plugin.cs b/BossMod/Framework/Plugin.cs index 161eb08f99..5d1bc6312b 100644 --- a/BossMod/Framework/Plugin.cs +++ b/BossMod/Framework/Plugin.cs @@ -79,7 +79,7 @@ public unsafe Plugin(IDalamudPluginInterface dalamud, ICommandManager commandMan _bossmod = new(_ws); _zonemod = new(_ws); _hintsBuilder = new(_ws, _bossmod, _zonemod); - _movementOverride = new(); + _movementOverride = new(dalamud); _amex = new(_ws, _hints, _movementOverride); _wsSync = new(_ws, _amex); _rotation = new(_rotationDB, _bossmod, _hints); diff --git a/BossMod/Framework/WorldStateGameSync.cs b/BossMod/Framework/WorldStateGameSync.cs index b012615d74..cffa7435b7 100644 --- a/BossMod/Framework/WorldStateGameSync.cs +++ b/BossMod/Framework/WorldStateGameSync.cs @@ -68,6 +68,9 @@ sealed class WorldStateGameSync : IDisposable private unsafe delegate void* ProcessSystemLogMessageDelegate(uint entityId, uint logMessageId, int* args, byte argCount); private readonly Hook _processSystemLogMessageHook; + private unsafe delegate void* ProcessPacketFateInfoDelegate(ulong fateId, long startTimestamp, ulong durationSecs); + private readonly Hook _processPacketFateInfoHook; + public unsafe WorldStateGameSync(WorldState ws, ActionManagerEx amex) { _ws = ws; @@ -124,6 +127,10 @@ public unsafe WorldStateGameSync(WorldState ws, ActionManagerEx amex) _processPacketOpenTreasureHook = Service.Hook.HookFromSignature("40 53 48 83 EC 20 48 8B DA 48 8D 0D ?? ?? ?? ?? 8B 52 10 E8 ?? ?? ?? ?? 48 85 C0 74 1B", ProcessPacketOpenTreasureDetour); _processPacketOpenTreasureHook.Enable(); Service.Log($"[WSG] ProcessPacketOpenTreasure address = 0x{_processPacketOpenTreasureHook.Address:X}"); + + _processPacketFateInfoHook = Service.Hook.HookFromSignature("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 0F B7 4B 10 48 8D 53 12 41 B8 ?? ?? ?? ??", ProcessPacketFateInfoDetour); + _processPacketFateInfoHook.Enable(); + Service.Log($"[WSG] ProcessPacketFateInfo address = 0x{_processPacketFateInfoHook.Address:X}"); } public void Dispose() @@ -137,6 +144,7 @@ public void Dispose() _processPacketRSVDataHook.Dispose(); _processSystemLogMessageHook.Dispose(); _processPacketOpenTreasureHook.Dispose(); + _processPacketFateInfoHook.Dispose(); _subscriptions.Dispose(); _netConfig.Dispose(); _interceptor.Dispose(); @@ -259,6 +267,7 @@ private unsafe void UpdateActor(GameObject* obj, int index, Actor? act) hpmp.MaxHP = chr->MaxHealth; hpmp.Shield = (uint)(chr->ShieldValue * 0.01f * hpmp.MaxHP); hpmp.CurMP = chr->Mana; + hpmp.MaxMP = chr->MaxMana; inCombat = chr->InCombat; } var targetable = obj->GetIsTargetable(); @@ -270,6 +279,8 @@ private unsafe void UpdateActor(GameObject* obj, int index, Actor? act) var eventState = obj->EventState; var radius = obj->GetRadius(); var mountId = chr != null ? chr->Mount.MountId : 0u; + var forayInfoPtr = chr != null ? chr->GetForayInfo() : null; + var forayInfo = forayInfoPtr == null ? default : new ActorForayInfo(forayInfoPtr->Level, forayInfoPtr->Element); if (act == null) { @@ -314,6 +325,8 @@ private unsafe void UpdateActor(GameObject* obj, int index, Actor? act) _ws.Execute(new ActorState.OpTarget(act.InstanceID, target)); if (act.MountId != mountId) _ws.Execute(new ActorState.OpMount(act.InstanceID, mountId)); + if (act.ForayInfo != forayInfo) + _ws.Execute(new ActorState.OpForayInfo(act.InstanceID, forayInfo)); DispatchActorEvents(act.InstanceID); @@ -905,4 +918,11 @@ private unsafe void ProcessPacketOpenTreasureDetour(uint actorID, byte* packet) _globalOps.Add(new WorldState.OpSystemLogMessage(messageId, new Span(args, argCount).ToArray())); return res; } + + private unsafe void* ProcessPacketFateInfoDetour(ulong fateId, long startTimestamp, ulong durationSecs) + { + var res = _processPacketFateInfoHook.Original(fateId, startTimestamp, durationSecs); + _globalOps.Add(new ClientState.OpFateInfo((uint)fateId, DateTimeOffset.FromUnixTimeSeconds(startTimestamp).UtcDateTime)); + return res; + } } diff --git a/BossMod/Modules/Global/DeepDungeon/AutoClear.cs b/BossMod/Modules/Global/DeepDungeon/AutoClear.cs index 7b483879bb..336c936c5b 100644 --- a/BossMod/Modules/Global/DeepDungeon/AutoClear.cs +++ b/BossMod/Modules/Global/DeepDungeon/AutoClear.cs @@ -304,11 +304,12 @@ public override void DrawExtra() ImGui.Text($"Kills: {Kills}"); - var navInCombat = _config.NavigateInCombat; + var maxPull = _config.MaxPull; - if (ImGui.Checkbox("Allow navigation in combat", ref navInCombat)) + ImGui.SetNextItemWidth(200); + if (ImGui.DragInt("Max mobs to pull", ref maxPull, 0.05f, 0, 15)) { - _config.NavigateInCombat = navInCombat; + _config.MaxPull = maxPull; _config.Modified.Fire(); } @@ -392,10 +393,14 @@ public override void CalculateAIHints(int playerSlot, Actor player, AIHints hint if (!_config.Enable || Palace.IsBossFloor || BetweenFloors) return; + var canNavigate = _config.MaxPull == 0 ? !player.InCombat : hints.PotentialTargets.Count(t => t.Actor.AggroPlayer && !t.Actor.IsDeadOrDestroyed) < _config.MaxPull; + foreach (var (w, rot) in Walls) hints.AddForbiddenZone(new AOEShapeRect(w.Depth, 20, w.Depth), w.Position, (rot ? 90f : 0f).Degrees()); - HandleFloorPathfind(player, hints); + if (canNavigate) + HandleFloorPathfind(player, hints); + DrawAOEs(playerSlot, player, hints); CalculateExtraHints(playerSlot, player, hints); @@ -458,7 +463,6 @@ public override void CalculateAIHints(int playerSlot, Actor player, AIHints hint } } - if (_config.TrapHints && _trapsHidden) { var traps = _trapsCurrentZone.Where(t => t.InCircle(player.Position, 30) && !IgnoreTraps.Any(b => b.AlmostEqual(t, 1))).Select(t => ShapeDistance.Circle(t, 2)).ToList(); @@ -488,7 +492,7 @@ public override void CalculateAIHints(int playerSlot, Actor player, AIHints hint hints.ActionsToExecute.Push(new ActionID(ActionType.Pomander, (uint)p2), null, ActionQueue.Priority.VeryHigh); Actor? wantCoffer = null; - if (coffer is Actor t && !IsPlayerTransformed(player) && (_config.AutoMoveTreasure && (!player.InCombat || _config.NavigateInCombat) || player.DistanceToHitbox(t) < 3.5f)) + if (coffer is Actor t && !IsPlayerTransformed(player) && (_config.AutoMoveTreasure && canNavigate || player.DistanceToHitbox(t) < 3.5f)) wantCoffer = t; if (!player.InCombat && _config.AutoPassage && Palace.PassageActive) @@ -516,7 +520,7 @@ public override void CalculateAIHints(int playerSlot, Actor player, AIHints hint if (revealedTraps.Count > 0) hints.AddForbiddenZone(ShapeDistance.Union(revealedTraps)); - if (!IsPlayerTransformed(player) && (!player.InCombat || _config.NavigateInCombat) && _config.AutoMoveTreasure && hoardLight is Actor h && Palace.GetPomanderState(PomanderID.Intuition).Active) + if (!IsPlayerTransformed(player) && canNavigate && _config.AutoMoveTreasure && hoardLight is Actor h && Palace.GetPomanderState(PomanderID.Intuition).Active) hints.GoalZones.Add(hints.GoalSingleTarget(h.Position, 2, 10)); var shouldTargetMobs = _config.AutoClear switch @@ -648,9 +652,6 @@ private void DrawAOEs(int playerSlot, Actor player, AIHints hints) private void HandleFloorPathfind(Actor player, AIHints hints) { - if (player.InCombat && !_config.NavigateInCombat) - return; - var playerRoom = Palace.Party[0].Room; if (DesiredRoom == playerRoom || DesiredRoom == 0) diff --git a/BossMod/Modules/Global/DeepDungeon/Config.cs b/BossMod/Modules/Global/DeepDungeon/Config.cs index e126593501..3ff6ea1870 100644 --- a/BossMod/Modules/Global/DeepDungeon/Config.cs +++ b/BossMod/Modules/Global/DeepDungeon/Config.cs @@ -27,8 +27,9 @@ public enum ClearBehavior [PropertyDisplay("Automatic mob targeting behavior")] public ClearBehavior AutoClear = ClearBehavior.Leveling; - [PropertyDisplay("Allow navigation in combat")] - public bool NavigateInCombat = false; + [PropertyDisplay("Max number of mobs to pull before pausing navigation (set to 0 to disable navigation while in combat)")] + [PropertySlider(0, 15)] + public int MaxPull = 0; [PropertyDisplay("Try to use terrain to LOS attacks")] public bool AutoLOS = false; diff --git a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D70Yaquaru.cs b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D70Yaquaru.cs index 4526ffad49..ec1d448326 100644 --- a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D70Yaquaru.cs +++ b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D70Yaquaru.cs @@ -16,6 +16,11 @@ public enum AID : uint FangsEnd = 7092, // Boss->player, no cast, single-target } +public enum SID : uint +{ + Heavy = 14 +} + class DouseCast(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Douse), new AOEShapeCircle(8)); class DousePuddle(BossModule module) : BossComponent(module) { @@ -55,6 +60,34 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } class Electrogenesis(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Electrogenesis), 8); +class FangsEnd(BossModule module) : BossComponent(module) +{ + private BitMask _heavy; + + public override void Update() + { + for (var i = 0; i < 4; i++) + { + var player = Raid[i]; + if (player == null) + continue; + + if (player.FindStatus(SID.Heavy) is ActorStatus st && (st.ExpireAt - WorldState.CurrentTime).TotalSeconds > 8) + _heavy.Set(i); + } + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if (status.ID == (uint)SID.Heavy) + _heavy.Clear(Raid.FindSlot(actor.InstanceID)); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + hints.ShouldCleanse |= _heavy; + } +} class D70TaquaruStates : StateMachineBuilder { @@ -63,7 +96,8 @@ public D70TaquaruStates(BossModule module) : base(module) TrivialPhase() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter(); + .ActivateOnEnter() + .ActivateOnEnter(); } } diff --git a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D90TheGodmother.cs b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D90TheGodmother.cs index 1f536bdc21..c114feb688 100644 --- a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D90TheGodmother.cs +++ b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D90TheGodmother.cs @@ -19,7 +19,7 @@ public enum AID : uint SelfDestruct = 7106, // LavaBomb->self, 3.0s cast, range 6+R circle } -class BossAdds(BossModule module) : Components.AddsMulti(module, [(uint)OID.GreyBomb, (uint)OID.GiddyBomb]); +class GreyBomb(BossModule module) : Components.Adds(module, (uint)OID.GreyBomb, 5); class Burst(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Burst), "Kill the Grey Bomb! or take 80% of your Max HP"); // future thing to do: maybe add a tether between bomb/boss to show it needs to show the aoe needs to explode on them. . . class HypothermalCombustion(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HypothermalCombustion), new AOEShapeCircle(7.2f)) @@ -32,6 +32,37 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme hints.SetPriority(g, AIHints.Enemy.PriorityForbidden); } } +class GiddyBomb(BossModule module) : BossComponent(module) +{ + public static readonly WPos[] BombSpawns = [ + new(-305, -240), + new(-295, -240), + new(-295, -240), + new(-300, -235) + ]; + + private int _index; + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.HypothermalCombustion) + _index++; + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // not tanking + if (Module.PrimaryActor.TargetID != actor.InstanceID) + return; + + // giddy bomb is alive, don't pull anywhere + if (Module.Enemies(OID.GiddyBomb).Any(x => !x.IsDeadOrDestroyed)) + return; + + var nextBombSpot = BombSpawns[_index % BombSpawns.Length]; + hints.GoalZones.Add(hints.PullTargetToLocation(Module.PrimaryActor, nextBombSpot)); + } +} class MassiveBurst(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.MassiveBurst), "Knock the Giddy bomb into the boss and let it explode on the boss. \n or else take 99% damage!"); class Sap(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Sap), 8); class ScaldingScolding(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.ScaldingScolding), new AOEShapeCone(11.75f, 45.Degrees())) @@ -83,7 +114,8 @@ class D90TheGodmotherStates : StateMachineBuilder public D90TheGodmotherStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Stormblood/Foray/Hydatos.cs b/BossMod/Modules/Stormblood/Foray/Hydatos.cs new file mode 100644 index 0000000000..a405de56fe --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/Hydatos.cs @@ -0,0 +1,167 @@ +using ImGuiNET; + +namespace BossMod.Stormblood.Foray.Hydatos; + +[ConfigDisplay(Name = "Eureka", Parent = typeof(StormbloodConfig))] +public class EurekaConfig : ConfigNode +{ + [PropertyDisplay("Max range to look for new mobs to pull")] + [PropertySlider(20, 100, Speed = 0.1f)] + public float MaxPullDistance = 30f; + + [PropertyDisplay("Max number of mobs to pull at once (0 for no limit)")] + [PropertySlider(0, 30, Speed = 0.1f)] + public int MaxPullCount = 10; +} + +[ConfigDisplay(Name = "Hydatos", Parent = typeof(EurekaConfig))] +public class HydatosConfig : ConfigNode +{ + public NotoriousMonster CurrentFarmTarget = NotoriousMonster.None; +} + +public enum NotoriousMonster : uint +{ + None, + [PropertyDisplay("Khalamari (Xzomit)")] + Khalamari, + [PropertyDisplay("Stegodon (Hydatos Primelephas)")] + Stego, + [PropertyDisplay("Molech (Val Nullchu)")] + Molech, + [PropertyDisplay("Piasa (Vivid Gastornis)")] + Piasa, + [PropertyDisplay("Frostmane (Northern Tiger)")] + Frostmane, + [PropertyDisplay("Daphne (Dark Void Monk)")] + Daphne, + [PropertyDisplay("Goldemar (Hydatos Wraith)")] + Golde, + [PropertyDisplay("Leuke (Tigerhawk)")] + Leuke, + [PropertyDisplay("Barong (Laboratory Lion)")] + Barong, + [PropertyDisplay("Ceto (Hydatos Delphyne)")] + Ceto, + [PropertyDisplay("PW (Crystal Claw)")] + PW +} + +static class NMExtensions +{ + public static uint GetMobID(this NotoriousMonster opt) => opt switch + { + NotoriousMonster.Khalamari => 0x26AB, + NotoriousMonster.Stego => 0x26AF, + NotoriousMonster.Molech => 0x26B2, + NotoriousMonster.Piasa => 0x26B3, + NotoriousMonster.Frostmane => 0x26B8, + NotoriousMonster.Daphne => 0x26B9, + NotoriousMonster.Golde => 0x26E6, + NotoriousMonster.Leuke => 0x26C0, + NotoriousMonster.Barong => 0x26C2, + NotoriousMonster.Ceto => 0x26C5, + NotoriousMonster.PW => 0x26CA, + _ => 0 + }; + + public static uint GetFateID(this NotoriousMonster opt) => opt switch + { + NotoriousMonster.Khalamari => 1412, + NotoriousMonster.Stego => 1413, + NotoriousMonster.Molech => 1414, + NotoriousMonster.Piasa => 1415, + NotoriousMonster.Frostmane => 1416, + NotoriousMonster.Daphne => 1417, + NotoriousMonster.Golde => 1418, + NotoriousMonster.Leuke => 1419, + NotoriousMonster.Barong => 1420, + NotoriousMonster.Ceto => 1421, + NotoriousMonster.PW => 1423, + _ => 0 + }; +} + +[ZoneModuleInfo(BossModuleInfo.Maturity.WIP, 639)] +public class Hydatos : ZoneModule +{ + private readonly EurekaConfig _eurekaConfig = Service.Config.Get(); + private readonly HydatosConfig _hydatosConfig = Service.Config.Get(); + + private readonly EventSubscriptions _subscriptions; + + public Hydatos(WorldState ws) : base(ws) + { + _subscriptions = new( + ws.Client.FateInfo.Subscribe(OnFateSpawned) + ); + } + + protected override void Dispose(bool disposing) + { + _subscriptions.Dispose(); + base.Dispose(disposing); + } + + private void OnFateSpawned(ClientState.OpFateInfo fate) + { + if (_hydatosConfig.CurrentFarmTarget.GetFateID() == fate.FateId) + { + _hydatosConfig.CurrentFarmTarget = NotoriousMonster.None; + _hydatosConfig.Modified.Fire(); + } + } + + public override void CalculateAIHints(int playerSlot, Actor player, AIHints hints) + { + hints.ForbiddenZones.RemoveAll(z => World.Actors.Find(z.Source) is Actor src && ShouldIgnore(src, player)); + + var farmOID = _hydatosConfig.CurrentFarmTarget.GetMobID(); + var farmMax = _eurekaConfig.MaxPullCount; + var farmRange = _eurekaConfig.MaxPullDistance; + + if (farmOID > 0 && (farmMax == 0 || hints.PotentialTargets.Count(e => e.Priority >= 0) < farmMax)) + foreach (var e in hints.PotentialTargets) + if (e.Actor.OID == farmOID && e.Priority == AIHints.Enemy.PriorityUndesirable && (e.Actor.Position - player.Position).LengthSq() <= farmRange * farmRange) + { + // level 60 hydatos wraith does not spawn golde + if (farmOID == 0x26E6 && e.Actor.ForayInfo.Level < 61) + continue; + + e.Priority = 0; + + var shouldSetTarget = !e.Actor.InCombat; + + if (shouldSetTarget && (hints.ForcedTarget == null || (hints.ForcedTarget.Position - player.Position).LengthSq() > (e.Actor.Position - player.Position).LengthSq())) + hints.ForcedTarget = e.Actor; + } + } + + private bool ShouldIgnore(Actor caster, Actor player) + { + return caster.CastInfo != null + && caster.CastInfo.Action.ID switch + { + 15415 or 15416 => true, + 15449 or 15295 => caster.CastInfo.TargetID == player.InstanceID, + _ => false, + }; + } + + public override bool WantDrawExtra() => true; + + public override string WindowName() => "Hydatos###Eureka module"; + + public override void DrawExtra() + { + if (UICombo.Enum("Prep", ref _hydatosConfig.CurrentFarmTarget)) + _hydatosConfig.Modified.Fire(); + + ImGui.SetNextItemWidth(200); + if (ImGui.DragFloat("Max distance to look for new mobs", ref _eurekaConfig.MaxPullDistance, 1, 20, 80)) + _eurekaConfig.Modified.Fire(); + ImGui.SetNextItemWidth(200); + if (ImGui.DragInt("Max mobs to pull (set to 0 for no limit)", ref _eurekaConfig.MaxPullCount, 1, 0, 30)) + _eurekaConfig.Modified.Fire(); + } +} diff --git a/BossMod/Modules/Stormblood/Foray/NM/Ceto.cs b/BossMod/Modules/Stormblood/Foray/NM/Ceto.cs index b9be6eea7d..41fff809d6 100644 --- a/BossMod/Modules/Stormblood/Foray/NM/Ceto.cs +++ b/BossMod/Modules/Stormblood/Foray/NM/Ceto.cs @@ -29,7 +29,7 @@ class CircleOfFlames(BossModule module) : Components.LocationTargetedAOEs(module class TailSlap(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TailSlap), new AOEShapeCone(12, 60.Degrees())); class Petrattraction(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.Petrattraction), 50, kind: Kind.TowardsOrigin); class CircleBlade(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CircleBlade), new AOEShapeCircle(7)); -class Adds(BossModule module) : Components.Adds(module, (uint)OID.FaithlessGuard); +class Adds(BossModule module) : Components.AddsPointless(module, (uint)OID.FaithlessGuard); class CetoStates : StateMachineBuilder { @@ -48,6 +48,6 @@ public CetoStates(BossModule module) : base(module) } } -[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 7955, Contributors = "xan")] +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.EurekaNM, GroupID = 639, NameID = 1421, Contributors = "xan", SortOrder = 9)] public class Ceto(WorldState ws, Actor primary) : BossModule(ws, primary, new(747.8959f, -878.8765f), new ArenaBoundsCircle(80, MapResolution: 1)); diff --git a/BossMod/Modules/Stormblood/Foray/NM/Daphne.cs b/BossMod/Modules/Stormblood/Foray/NM/Daphne.cs index eac8a65344..cc5f11f874 100644 --- a/BossMod/Modules/Stormblood/Foray/NM/Daphne.cs +++ b/BossMod/Modules/Stormblood/Foray/NM/Daphne.cs @@ -37,6 +37,6 @@ public DaphneStates(BossModule module) : base(module) } } -[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 7967, Contributors = "xan")] +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.EurekaNM, GroupID = 639, NameID = 1417, Contributors = "xan", SortOrder = 5)] public class Daphne(WorldState ws, Actor primary) : BossModule(ws, primary, new(207.8475f, -736.8179f), new ArenaBoundsCircle(80, MapResolution: 1)); diff --git a/BossMod/Modules/Stormblood/Foray/NM/Molech.cs b/BossMod/Modules/Stormblood/Foray/NM/Molech.cs index dd0293b6d7..131dfbae98 100644 --- a/BossMod/Modules/Stormblood/Foray/NM/Molech.cs +++ b/BossMod/Modules/Stormblood/Foray/NM/Molech.cs @@ -1,19 +1,44 @@ -//namespace BossMod.Stormblood.Foray.NM.Molech; +namespace BossMod.Stormblood.Foray.NM.Molech; -//public enum OID : uint -//{ -// Boss = 0x275D, -// Helper = 0x233C, -//} +public enum OID : uint +{ + Boss = 0x275D, // R6.000, x1 + Adulator = 0x275E, // R2.800, x3 +} -//class MolechStates : StateMachineBuilder -//{ -// public MolechStates(BossModule module) : base(module) -// { -// TrivialPhase(); -// } -//} +public enum AID : uint +{ + W11TonzeSwipeAdds = 14978, // Adulator->player, no cast, single-target + W11TonzeSwipe = 14972, // Boss->self, 3.0s cast, range 9 ?-degree cone + W111TonzeSwing = 14973, // Boss->self, 4.0s cast, range 13 circle + OrderToStandFast = 14976, // Boss->self, 3.0s cast, range 100 circle + W111TonzeSwingAdds = 14979, // Adulator->self, 3.0s cast, range 13 circle + W111TonzeSwingBig = 14974, // Boss->self, 4.0s cast, range 20 circle + OrderToAssault = 14975, // Boss->self, 3.0s cast, range 100 circle + ZoomIn = 14980, // Adulator->location, 3.0s cast, width 8 rect charge +} -//[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 8070)] -//public class Molech(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsCircle(20)); +class Adds(BossModule module) : Components.AddsPointless(module, (uint)OID.Adulator); +class W11TonzeSwipe(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W11TonzeSwipe), new AOEShapeCone(9, 75.Degrees())); +class W111TonzeSwing(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W111TonzeSwing), new AOEShapeCircle(13)); +class W111TonzeSwingAdds(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W111TonzeSwingAdds), new AOEShapeCircle(13)); +class W111TonzeSwingBig(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W111TonzeSwingBig), new AOEShapeCircle(20)); +class ZoomIn(BossModule module) : Components.ChargeAOEs(module, ActionID.MakeSpell(AID.ZoomIn), 4); + +class MolechStates : StateMachineBuilder +{ + public MolechStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.EurekaNM, GroupID = 639, NameID = 1414, Contributors = "xan", SortOrder = 3)] +public class Molech(WorldState ws, Actor primary) : BossModule(ws, primary, new(-676.8632f, -441.8009f), new ArenaBoundsCircle(80, MapResolution: 1)); diff --git a/BossMod/Modules/Stormblood/Foray/NM/Ovni.cs b/BossMod/Modules/Stormblood/Foray/NM/Ovni.cs index 0b353e10de..b657b3175f 100644 --- a/BossMod/Modules/Stormblood/Foray/NM/Ovni.cs +++ b/BossMod/Modules/Stormblood/Foray/NM/Ovni.cs @@ -73,6 +73,6 @@ public OvniStates(BossModule module) : base(module) } } -[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 8060, Contributors = "xan")] +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.EurekaNM, GroupID = 639, NameID = 1424, Contributors = "xan", SortOrder = 11)] public class Ovni(WorldState ws, Actor primary) : BossModule(ws, primary, new(266.1068f, -97.09414f), new ArenaBoundsCircle(80, MapResolution: 1)); diff --git a/BossMod/Replay/ReplayParserLog.cs b/BossMod/Replay/ReplayParserLog.cs index d58de0f42c..ae1042514d 100644 --- a/BossMod/Replay/ReplayParserLog.cs +++ b/BossMod/Replay/ReplayParserLog.cs @@ -313,6 +313,7 @@ private ReplayParserLog(Input input, ReplayBuilder builder) [new("EVTS"u8)] = ParseActorEventState, [new("TARG"u8)] = ParseActorTarget, [new("MNTD"u8)] = ParseActorMount, + [new("FORA"u8)] = ParseActorForay, [new("TETH"u8)] = () => ParseActorTether(true), [new("TET+"u8)] = () => ParseActorTether(true), // legacy (up to v4) [new("TET-"u8)] = () => ParseActorTether(false), // legacy (up to v4) @@ -353,6 +354,7 @@ private ReplayParserLog(Input input, ReplayBuilder builder) [new("CLFT"u8)] = ParseClientFocusTarget, [new("CLFD"u8)] = ParseClientForcedMovementDirection, [new("CLKV"u8)] = ParseClientContentKVData, + [new("FATE"u8)] = ParseClientFateInfo, [new("DDPG"u8)] = ParseDeepDungeonProgress, [new("DDMP"u8)] = ParseDeepDungeonMap, [new("DDPT"u8)] = ParseDeepDungeonParty, @@ -460,6 +462,8 @@ private WorldState.OpSystemLogMessage ParseSystemLog() return new(id, args); } + private ClientState.OpFateInfo ParseClientFateInfo() => new(_input.ReadUInt(false), new(_input.ReadLong())); + private WaymarkState.OpWaymarkChange ParseWaymarkChange(bool set) => new(_version < 10 ? Enum.Parse(_input.ReadString()) : (Waymark)_input.ReadByte(false), set ? _input.ReadVec3() : null); @@ -508,7 +512,7 @@ private ActorState.OpCreate ParseActorCreate() _version < 12 ? 0 : _input.ReadInt(), new(_input.ReadVec3(), _input.ReadAngle().Rad), _input.ReadFloat(), - new(_input.ReadUInt(false), _input.ReadUInt(false), _input.ReadUInt(false), _input.ReadUInt(false)), + new(_input.ReadUInt(false), _input.ReadUInt(false), _input.ReadUInt(false), _input.ReadUInt(false), _version >= 24 ? _input.ReadUInt(false) : 10000), _input.ReadBool(), _input.ReadBool(), _input.ReadActorID(), @@ -564,6 +568,7 @@ private ActorState.OpModelState ParseActorModelState() private ActorState.OpEventState ParseActorEventState() => new(_input.ReadActorID(), _input.ReadByte(false)); private ActorState.OpTarget ParseActorTarget() => new(_input.ReadActorID(), _input.ReadActorID()); private ActorState.OpMount ParseActorMount() => new(_input.ReadActorID(), _input.ReadUInt(false)); + private ActorState.OpForayInfo ParseActorForay() => new(_input.ReadActorID(), new(_input.ReadByte(false), _input.ReadByte(false))); private ActorState.OpTether ParseActorTether(bool tether) => new(_input.ReadActorID(), tether ? new(_input.ReadUInt(false), _input.ReadActorID()) : default); private ActorState.OpCastInfo ParseActorCastInfo(bool start) @@ -762,11 +767,11 @@ private ActorHPMP ReadActorHPMP() if (_version < 10) { var parts = _input.ReadString().Split('/'); - return new(uint.Parse(parts[0]), uint.Parse(parts[1]), parts.Length > 2 ? uint.Parse(parts[2]) : 0, parts.Length > 3 ? uint.Parse(parts[3]) : 0); + return new(uint.Parse(parts[0]), uint.Parse(parts[1]), parts.Length > 2 ? uint.Parse(parts[2]) : 0, parts.Length > 3 ? uint.Parse(parts[3]) : 0, 10000); } else { - return new(_input.ReadUInt(false), _input.ReadUInt(false), _input.ReadUInt(false), _input.ReadUInt(false)); + return new(_input.ReadUInt(false), _input.ReadUInt(false), _input.ReadUInt(false), _input.ReadUInt(false), _version >= 24 ? _input.ReadUInt(false) : 10000); } } } diff --git a/BossMod/Replay/ReplayRecorder.cs b/BossMod/Replay/ReplayRecorder.cs index cef4662b2d..43fea1e165 100644 --- a/BossMod/Replay/ReplayRecorder.cs +++ b/BossMod/Replay/ReplayRecorder.cs @@ -176,7 +176,7 @@ public override void EndEntry() { } private readonly Output _logger; private readonly EventSubscription _subscription; - public const int Version = 23; + public const int Version = 24; public ReplayRecorder(WorldState ws, ReplayLogFormat format, bool logInitialState, DirectoryInfo targetDirectory, string logPrefix) {