From 4919e1ad5ea6f82eeee2c6505b2e960883998ae3 Mon Sep 17 00:00:00 2001 From: LTS-FFXIV <127939494+LTS-FFXIV@users.noreply.github.com> Date: Wed, 15 Jan 2025 07:54:53 -0600 Subject: [PATCH] Refactor and enhance rotation logic and configurations Refactored SCH, SGE, and WHM rotation logic for better readability and maintainability. Updated `SCH_Default.cs` and `SGE_Default.cs` to iterate over party members and tanks for specific actions. Simplified and refactored methods in `SGE_Default.cs` to remove LINQ. Updated `ECommons` subproject commit. Removed `IsLastAbilityUsable` and `IsFirstAbilityUsable` checks in `BaseAction.cs`. Added new configuration options in `Configs.cs` for raising players and adjusted `_action4head` value. Updated `DataCenter.cs` with XML documentation comments and adjusted `CalculatedActionAhead`. Refactored `StatusHelper` and `TargetFilter` methods to use explicit loops instead of LINQ. Improved `CustomRotation` logic with additional checks and conditions. Commented out `UseLimitBreak` method in `CustomRotation_GCD.cs`. Added null checks and early returns in `CustomRotation` methods. Cleaned up debug text in `RotationConfigWindow`. Simplified `RemoveExpiredVfxData` method in `MajorUpdater`. Enhanced `TargetUpdater` with explicit loops, new configuration checks, and improved logic for determining valid raise targets. --- BasicRotations/Healer/SCH_Default.cs | 4 +- BasicRotations/Healer/SGE_Default.cs | 140 +++++++++--------- BasicRotations/Healer/WHM_Default.cs | 6 +- ECommons | 2 +- RotationSolver.Basic/Actions/BaseAction.cs | 31 ---- RotationSolver.Basic/Configuration/Configs.cs | 22 ++- RotationSolver.Basic/DataCenter.cs | 51 ++++--- RotationSolver.Basic/Helpers/StatusHelper.cs | 51 +++++-- RotationSolver.Basic/Helpers/TargetFilter.cs | 73 +++++++-- .../Rotations/CustomRotation_Ability.cs | 4 +- .../Rotations/CustomRotation_GCD.cs | 47 +++--- RotationSolver/UI/RotationConfigWindow.cs | 30 ---- RotationSolver/Updaters/MajorUpdater.cs | 19 ++- RotationSolver/Updaters/TargetUpdater.cs | 47 ++++-- 14 files changed, 295 insertions(+), 232 deletions(-) diff --git a/BasicRotations/Healer/SCH_Default.cs b/BasicRotations/Healer/SCH_Default.cs index 41e4c30e6..ae24b15b0 100644 --- a/BasicRotations/Healer/SCH_Default.cs +++ b/BasicRotations/Healer/SCH_Default.cs @@ -180,7 +180,7 @@ protected override bool HealAreaGCD(out IAction? act) return base.HealAreaGCD(out act); } - [RotationDesc(ActionID.AdloquiumPvE, ActionID.PhysickPvE)] + [RotationDesc(ActionID.AdloquiumPvE, ActionID.ManifestationPvE, ActionID.PhysickPvE)] protected override bool HealSingleGCD(out IAction? act) { act = null; @@ -193,7 +193,7 @@ protected override bool HealSingleGCD(out IAction? act) return base.HealSingleGCD(out act); } - [RotationDesc(ActionID.SuccorPvE)] + [RotationDesc(ActionID.SuccorPvE, ActionID.ConcitationPvE, ActionID.AccessionPvE)] protected override bool DefenseAreaGCD(out IAction? act) { act = null; diff --git a/BasicRotations/Healer/SGE_Default.cs b/BasicRotations/Healer/SGE_Default.cs index 6b6382bb5..91668c716 100644 --- a/BasicRotations/Healer/SGE_Default.cs +++ b/BasicRotations/Healer/SGE_Default.cs @@ -6,9 +6,6 @@ namespace RebornRotations.Healer; public sealed class SGE_Default : SageRotation { #region Config Options - [RotationConfig(CombatType.PvE, Name = "Use new Eukrasian Logic")] - public bool NewELogic { get; set; } = true; - [RotationConfig(CombatType.PvE, Name = "Use spells with cast times to heal. (Ignored if you are the only healer in party)")] public bool GCDHeal { get; set; } = false; @@ -93,7 +90,6 @@ protected override bool EmergencyAbility(IAction nextGCD, out IAction? act) } if (ChoiceEukrasia(out act)) return true; - //if (base.EmergencyAbility(nextGCD, out act)) return true; if (nextGCD.IsTheSameTo(false, PneumaPvE, EukrasianPrognosisPvE, EukrasianPrognosisIiPvE)) @@ -193,29 +189,49 @@ protected override bool HealSingleAbility(IAction nextGCD, out IAction? act) if ((!TaurocholePvE.EnoughLevel || TaurocholePvE.Cooldown.IsCoolingDown) && DruocholePvE.CanUse(out act)) return true; - if (SoteriaPvE.CanUse(out act) && PartyMembers.Any(b => b.HasStatus(true, StatusID.Kardion) && b.GetHealthRatio() < SoteriaHeal)) return true; + foreach (var member in PartyMembers) + { + if (SoteriaPvE.CanUse(out act) && member.HasStatus(true, StatusID.Kardion) && member.GetHealthRatio() < SoteriaHeal) + { + return true; + } + } var tank = PartyMembers.GetJobCategory(JobRole.Tank); - if (Addersgall < 1 && (tank.Any(t => t.GetHealthRatio() < OGCDTankHeal) || PartyMembers.Any(b => b.GetHealthRatio() < OGCDHeal))) + foreach (var t in tank) { - if (HaimaPvE.CanUse(out act)) return true; - - if (PhysisIiPvE.CanUse(out act)) return true; - if (!PhysisIiPvE.EnoughLevel && PhysisPvE.CanUse(out act)) return true; - - if (HolosPvE.CanUse(out act)) return true; + if (Addersgall < 1 && t.GetHealthRatio() < OGCDTankHeal) + { + if (HaimaPvE.CanUse(out act)) return true; + if (PhysisIiPvE.CanUse(out act)) return true; + if (!PhysisIiPvE.EnoughLevel && PhysisPvE.CanUse(out act)) return true; + if (HolosPvE.CanUse(out act)) return true; + if ((!HaimaPvE.EnoughLevel || HaimaPvE.Cooldown.ElapsedAfter(20)) && PanhaimaPvE.CanUse(out act)) return true; + } + } - if ((!HaimaPvE.EnoughLevel || HaimaPvE.Cooldown.ElapsedAfter(20)) && PanhaimaPvE.CanUse(out act)) return true; + foreach (var t in tank) + { + if (t.GetHealthRatio() < ZoeHeal) + { + if (ZoePvE.CanUse(out act)) return true; + } } - if (tank.Any(t => t.GetHealthRatio() < ZoeHeal)) + foreach (var t in tank) { - if (ZoePvE.CanUse(out act)) return true; + if (t.GetHealthRatio() < KrasisTankHeal) + { + if (KrasisPvE.CanUse(out act)) return true; + } } - if (tank.Any(t => t.GetHealthRatio() < KrasisTankHeal) || PartyMembers.Any(b => b.GetHealthRatio() < KrasisHeal)) + foreach (var member in PartyMembers) { - if (KrasisPvE.CanUse(out act)) return true; + if (member.GetHealthRatio() < KrasisHeal) + { + if (KrasisPvE.CanUse(out act)) return true; + } } return base.HealSingleAbility(nextGCD, out act); @@ -246,14 +262,9 @@ protected override bool GeneralAbility(IAction nextGCD, out IAction? act) // Finally, updates the current Eukrasia action aim if it's different from the incoming action. private void SetEukrasia(IBaseAction act) { - if (act == null) return; - - if (_EukrasiaActionAim != null && IsLastGCD(true, _EukrasiaActionAim)) return; + if (act == null || (_EukrasiaActionAim != null && IsLastGCD(true, _EukrasiaActionAim))) return; - if (_EukrasiaActionAim != act) - { - _EukrasiaActionAim = act; - } + _EukrasiaActionAim = act; } // Clears the Eukrasia action aim, effectively resetting any planned Eukrasia action. @@ -273,69 +284,45 @@ private bool ChoiceEukrasia(out IAction? act) if (!EukrasiaPvE.CanUse(out _)) return false; // Checks for Eukrasia status. - // Attempts to set correct Eurkrasia action based on availablity and MergedStatus. + // Attempts to set correct Eurkrasia action based on availability and MergedStatus. if (EukrasianPrognosisIiPvE.CanUse(out _) && EukrasianPrognosisIiPvE.EnoughLevel && MergedStatus.HasFlag(AutoStatus.DefenseArea)) { SetEukrasia(EukrasianPrognosisIiPvE); - return false; } - - if (EukrasianPrognosisPvE.CanUse(out _) && EukrasianPrognosisPvE.EnoughLevel && MergedStatus.HasFlag(AutoStatus.DefenseArea)) + else if (EukrasianPrognosisPvE.CanUse(out _) && EukrasianPrognosisPvE.EnoughLevel && MergedStatus.HasFlag(AutoStatus.DefenseArea)) { SetEukrasia(EukrasianPrognosisPvE); - return false; } - - if (EukrasianDiagnosisPvE.CanUse(out _) && EukrasianDiagnosisPvE.EnoughLevel && MergedStatus.HasFlag(AutoStatus.DefenseSingle)) + else if (EukrasianDiagnosisPvE.CanUse(out _) && EukrasianDiagnosisPvE.EnoughLevel && MergedStatus.HasFlag(AutoStatus.DefenseSingle)) { SetEukrasia(EukrasianDiagnosisPvE); - return false; } - - if (EukrasianDyskrasiaPvE.CanUse(out _) && EukrasianDyskrasiaPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseArea))) + else if (EukrasianDyskrasiaPvE.CanUse(out _) && EukrasianDyskrasiaPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseArea))) { SetEukrasia(EukrasianDyskrasiaPvE); - return false; } - - if ((!EukrasianDyskrasiaPvE.CanUse(out _) || !DyskrasiaPvE.CanUse(out _)) && EukrasianDosisIiiPvE.CanUse(out _) && EukrasianDosisIiiPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseArea))) + else if ((!EukrasianDyskrasiaPvE.CanUse(out _) || !DyskrasiaPvE.CanUse(out _)) && EukrasianDosisIiiPvE.CanUse(out _) && EukrasianDosisIiiPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseArea))) { SetEukrasia(EukrasianDosisIiiPvE); - return false; } - - if ((!EukrasianDyskrasiaPvE.CanUse(out _) || !DyskrasiaPvE.CanUse(out _)) && EukrasianDosisIiPvE.CanUse(out _) && EukrasianDosisIiPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseArea))) + else if ((!EukrasianDyskrasiaPvE.CanUse(out _) || !DyskrasiaPvE.CanUse(out _)) && EukrasianDosisIiPvE.CanUse(out _) && EukrasianDosisIiPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseArea))) { SetEukrasia(EukrasianDosisIiPvE); - return false; } - - if ((!EukrasianDyskrasiaPvE.CanUse(out _) || !DyskrasiaPvE.CanUse(out _)) && EukrasianDosisPvE.CanUse(out _) && EukrasianDosisPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseArea))) + else if ((!EukrasianDyskrasiaPvE.CanUse(out _) || !DyskrasiaPvE.CanUse(out _)) && EukrasianDosisPvE.CanUse(out _) && EukrasianDosisPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseArea))) { SetEukrasia(EukrasianDosisPvE); - return false; } - - return false; // Indicates that no specific Eukrasia action was chosen in this cycle. - } - #endregion - - #region Eukrasia Execution - // Attempts to perform a Eukrasia action, based on the current game state and conditions. - private bool DoEukrasia(out IAction? act) - { - act = null; - - if (_EukrasiaActionAim != null && _EukrasiaActionAim.CanUse(out act)) + else { - if (EukrasiaPvE.CanUse(out act)) return true; - - act = _EukrasiaActionAim; - return true; + return false; // Indicates that no specific Eukrasia action was chosen in this cycle. } + return false; } + #endregion + #region Eukrasia Execution // Attempts to perform a Eukrasia action, based on the current game state and conditions. private bool DoEukrasianPrognosis(out IAction? act) { @@ -402,7 +389,7 @@ private bool DoEukrasianDosis(out IAction? act) protected override bool HealAreaGCD(out IAction? act) { act = null; - if (HasSwift && SwiftLogic && MergedStatus.HasFlag(AutoStatus.Raise)) return false; + if (IsLastAction(ActionID.SwiftcastPvE) && SwiftLogic && MergedStatus.HasFlag(AutoStatus.Raise)) return false; if (PartyMembersAverHP < PneumaAOEPartyHeal || DyskrasiaPvE.CanUse(out _) && PartyMembers.GetJobCategory(JobRole.Tank).Any(t => t.GetHealthRatio() < PneumaAOETankHeal)) { @@ -421,8 +408,7 @@ protected override bool HealAreaGCD(out IAction? act) protected override bool HealSingleGCD(out IAction? act) { act = null; - if (HasSwift && SwiftLogic && MergedStatus.HasFlag(AutoStatus.Raise)) return false; - + if (IsLastAction(ActionID.SwiftcastPvE) && SwiftLogic && MergedStatus.HasFlag(AutoStatus.Raise)) return false; if (_EukrasiaActionAim != null && DiagnosisPvE.CanUse(out act)) { return true; @@ -433,27 +419,37 @@ protected override bool HealSingleGCD(out IAction? act) protected override bool GeneralGCD(out IAction? act) { act = null; - if (HasSwift && SwiftLogic && MergedStatus.HasFlag(AutoStatus.Raise)) return false; + if (IsLastAction(ActionID.SwiftcastPvE) && SwiftLogic && MergedStatus.HasFlag(AutoStatus.Raise)) return false; if (PhlegmaPvE.CanUse(out act, usedUp: IsMoving)) return true; - if (PartyMembers.Any(b => b.GetHealthRatio() < PneumaSTPartyHeal && !b.IsDead) || PartyMembers.GetJobCategory(JobRole.Tank).Any(t => t.GetHealthRatio() < PneumaSTTankHeal && !t.IsDead)) + foreach (var member in PartyMembers) { - if (PneumaPvE.CanUse(out act)) return true; + if (member.GetHealthRatio() < PneumaSTPartyHeal && !member.IsDead) + { + if (PneumaPvE.CanUse(out act)) return true; + } + } + + foreach (var tank in PartyMembers.GetJobCategory(JobRole.Tank)) + { + if (tank.GetHealthRatio() < PneumaSTTankHeal && !tank.IsDead) + { + if (PneumaPvE.CanUse(out act)) return true; + } } if (IsMoving && ToxikonPvE.CanUse(out act)) return true; - if (NewELogic && DoEukrasianDyskrasia(out act)) return true; + if (DoEukrasianDyskrasia(out act)) return true; - if ((_EukrasiaActionAim != EukrasianDiagnosisPvE || _EukrasiaActionAim != EukrasianPrognosisPvE || _EukrasiaActionAim != EukrasianPrognosisIiPvE || _EukrasiaActionAim != EukrasianDyskrasiaPvE) + if ((_EukrasiaActionAim != EukrasianDiagnosisPvE || _EukrasiaActionAim != EukrasianPrognosisPvE || _EukrasiaActionAim != EukrasianPrognosisIiPvE || _EukrasiaActionAim != EukrasianDyskrasiaPvE) && DyskrasiaPvE.CanUse(out act)) return true; - if (NewELogic && DoEukrasianPrognosis(out act)) return true; - if (NewELogic && DoEukrasianDiagnosis(out act)) return true; - if (!NewELogic && DoEukrasia(out act)) return true; + if (DoEukrasianPrognosis(out act)) return true; + if (DoEukrasianDiagnosis(out act)) return true; - if (NewELogic && DoEukrasianDosis(out act)) return true; + if ( DoEukrasianDosis(out act)) return true; if (DosisPvE.CanUse(out act)) return true; if (!InCombat && !Player.HasStatus(true, StatusID.Eukrasia) && EukrasiaPvE.CanUse(out act)) return true; diff --git a/BasicRotations/Healer/WHM_Default.cs b/BasicRotations/Healer/WHM_Default.cs index 2530725bd..7faecc9fc 100644 --- a/BasicRotations/Healer/WHM_Default.cs +++ b/BasicRotations/Healer/WHM_Default.cs @@ -170,7 +170,7 @@ protected override bool AttackAbility(IAction nextGCD, out IAction? act) protected override bool HealAreaGCD(out IAction? act) { act = null; - if (HasSwift && SwiftLogic && MergedStatus.HasFlag(AutoStatus.Raise)) return false; + if ((HasSwift || IsLastAction(ActionID.SwiftcastPvE)) && SwiftLogic && MergedStatus.HasFlag(AutoStatus.Raise)) return false; if (AfflatusRapturePvE.CanUse(out act)) return true; @@ -189,7 +189,7 @@ protected override bool HealAreaGCD(out IAction? act) protected override bool HealSingleGCD(out IAction? act) { act = null; - if (HasSwift && SwiftLogic && MergedStatus.HasFlag(AutoStatus.Raise)) return false; + if ((HasSwift || IsLastAction(ActionID.SwiftcastPvE)) && SwiftLogic && MergedStatus.HasFlag(AutoStatus.Raise)) return false; if (AfflatusSolacePvE.CanUse(out act)) return true; @@ -205,7 +205,7 @@ protected override bool HealSingleGCD(out IAction? act) protected override bool GeneralGCD(out IAction? act) { act = null; - if (HasSwift && SwiftLogic && MergedStatus.HasFlag(AutoStatus.Raise)) return false; + if ((HasSwift || IsLastAction(ActionID.SwiftcastPvE)) && SwiftLogic && MergedStatus.HasFlag(AutoStatus.Raise)) return false; //if (NotInCombatDelay && RegenDefense.CanUse(out act)) return true; diff --git a/ECommons b/ECommons index 3de0ce6a4..2c01433db 160000 --- a/ECommons +++ b/ECommons @@ -1 +1 @@ -Subproject commit 3de0ce6a4acb79e058a2ab09ccbc9113c8b15bfc +Subproject commit 2c01433dba269cd7624b88e86cc8b74f904b34c7 diff --git a/RotationSolver.Basic/Actions/BaseAction.cs b/RotationSolver.Basic/Actions/BaseAction.cs index c1d0ddd95..1f2608fd7 100644 --- a/RotationSolver.Basic/Actions/BaseAction.cs +++ b/RotationSolver.Basic/Actions/BaseAction.cs @@ -142,9 +142,6 @@ public bool CanUse(out IAction act, bool isLastAbility = false, bool isFirstAbil usedUp = true; } - if (isLastAbility && !IsLastAbilityUsable()) return false; - if (isFirstAbility && !IsFirstAbilityUsable()) return false; - if (!Info.BasicCheck(skipStatusProvideCheck, skipComboCheck, skipCastingCheck)) return false; if (!Cooldown.CooldownCheck(usedUp, gcdCountForAbility)) return false; @@ -167,34 +164,6 @@ public bool CanUse(out IAction act, bool isLastAbility = false, bool isFirstAbil return true; } - private bool IsLastAbilityUsable() - { - if (Service.Config.UseV2AbilityChecks) - { - return IsLastAbilityv2Usable(); - } - return DataCenter.InCombat && (DataCenter.NextAbilityToNextGCD <= Math.Max(ActionManagerHelper.GetCurrentAnimationLock(), DataCenter.MinAnimationLock) + Service.Config.IsLastAbilityTimer); - } - - private bool IsFirstAbilityUsable() - { - if (Service.Config.UseV2AbilityChecks) - { - return IsFirstAbilityv2Usable(); - } - return DataCenter.InCombat && (DataCenter.NextAbilityToNextGCD >= Math.Max(ActionManagerHelper.GetCurrentAnimationLock(), DataCenter.MinAnimationLock) + Service.Config.IsFirstAbilityTimer); - } - - private bool IsLastAbilityv2Usable() - { - return DataCenter.InCombat && (DataCenter.DefaultGCDElapsed >= DataCenter.DefaultGCDRemain); - } - - private bool IsFirstAbilityv2Usable() - { - return DataCenter.InCombat && (DataCenter.DefaultGCDRemain >= DataCenter.DefaultGCDElapsed); - } - private bool IsTimeToKillValid() { return DataCenter.AverageTimeToKill >= Config.TimeToKill && DataCenter.AverageTimeToKill >= Config.TimeToUntargetable; diff --git a/RotationSolver.Basic/Configuration/Configs.cs b/RotationSolver.Basic/Configuration/Configs.cs index 341395275..aab446155 100644 --- a/RotationSolver.Basic/Configuration/Configs.cs +++ b/RotationSolver.Basic/Configuration/Configs.cs @@ -347,10 +347,6 @@ public const string Filter = Extra)] private static readonly bool _autoOpenChest = true; - [ConditionBool, UI("Use experimental FirstAbility and LastAbility checks", - Filter = Extra)] - private static readonly bool _useV2AbilityChecks = false; - [ConditionBool, UI("Enable RSR click counter in main menu", Filter = Extra)] private static readonly bool _enableClickingCount = true; @@ -445,6 +441,22 @@ public const string PvEFilter = JobFilterType.Raise, PvPFilter = JobFilterType.NoJob)] private static readonly bool _raiseBrinkOfDeath = true; + [JobConfig, UI("Raise non-Healers from bottom of party list to the top (Light Party 2 Healer Behaviour, Experimental)", + Filter = HealingActionCondition, Section = 2, + PvEFilter = JobFilterType.Raise, PvPFilter = JobFilterType.NoJob)] + private static readonly bool _h2 = false; + + [JobConfig, UI("Raise Red Mage and Summoners first if no Tanks or Healers are dead (Experimental)", + Filter = HealingActionCondition, Section = 2, + PvEFilter = JobFilterType.Raise, PvPFilter = JobFilterType.NoJob)] + private static readonly bool _offRaiserRaise = false; + + [JobConfig, UI("How early before next GCD should RSR use swiftcast for raise (Experimental)", + Filter = HealingActionCondition, Section = 2, + PvEFilter = JobFilterType.Raise, PvPFilter = JobFilterType.NoJob)] + [Range(0, 1.0f, ConfigUnitType.Seconds, 0.01f)] + public float SwiftcastBuffer { get; set; } = 0.6f; + [UI("Random delay range for resurrecting players.", Filter = HealingActionCondition, Section = 2, PvEFilter = JobFilterType.Raise, PvPFilter = JobFilterType.NoJob)] @@ -706,7 +718,7 @@ public const string [UI("Action Ahead (How far in advance of GCD being available RSR will try to queue the next GCD)", Description = "This setting controls how many oGCDs RSR will try to fit in a single GCD window\nLower numbers mean more oGCDs, but potentially more GCD clipping", Parent = nameof(OverrideActionAheadTimer))] - private readonly float _action4head = 0.4f; + private readonly float _action4head = 0.3f; [JobConfig] private readonly string _PvPRotationChoice = string.Empty; diff --git a/RotationSolver.Basic/DataCenter.cs b/RotationSolver.Basic/DataCenter.cs index f93b4c629..9b6e6b06e 100644 --- a/RotationSolver.Basic/DataCenter.cs +++ b/RotationSolver.Basic/DataCenter.cs @@ -244,37 +244,46 @@ public static unsafe ushort FateId } #region GCD - // Returns the time remaining until the next GCD (Global Cooldown) after considering the current animation lock. - public static float NextAbilityToNextGCD => DefaultGCDRemain - Math.Max(ActionManagerHelper.GetCurrentAnimationLock(), DataCenter.MinAnimationLock); + /// + /// Returns the time remaining until the next GCD (Global Cooldown) after considering the current animation lock. + /// + public static float NextAbilityToNextGCD => DefaultGCDRemain - Math.Min(ActionManagerHelper.GetCurrentAnimationLock(), MinAnimationLock); - // Returns the total duration of the default GCD. + /// + /// Returns the total duration of the default GCD. + /// public static float DefaultGCDTotal => ActionManagerHelper.GetDefaultRecastTime(); - // Returns the remaining time for the default GCD by subtracting the elapsed time from the total recast time. - public static float DefaultGCDRemain => - ActionManagerHelper.GetDefaultRecastTime() - ActionManagerHelper.GetDefaultRecastTimeElapsed(); + /// + /// Returns the remaining time for the default GCD by subtracting the elapsed time from the total recast time. + /// + public static float DefaultGCDRemain => DefaultGCDTotal - DefaultGCDElapsed; - // Returns the elapsed time since the start of the default GCD. + /// + /// Returns the elapsed time since the start of the default GCD. + /// public static float DefaultGCDElapsed => ActionManagerHelper.GetDefaultRecastTimeElapsed(); - // Returns the action ahead time, which can be overridden by a configuration setting. - public static float ActionAhead => - Service.Config.OverrideActionAheadTimer ? Service.Config.Action4Head : CalculatedActionAhead; - - // Returns the calculated action ahead time as 25% of the total GCD time. - public static float CalculatedActionAhead => Math.Min(DefaultGCDTotal * 0.25f, DataCenter.MinAnimationLock); - - // Calculates the total GCD time for a given number of GCDs and an optional offset. - public static float GCDTime(uint gcdCount = 0, float offset = 0) - => ActionManagerHelper.GetDefaultRecastTime() * gcdCount + offset; + /// + /// Returns the action ahead time, which can be overridden by a configuration setting. + /// + public static float ActionAhead => Service.Config.OverrideActionAheadTimer ? Service.Config.Action4Head : CalculatedActionAhead; - public static bool LastAbilityv2 => DataCenter.InCombat && !ActionHelper.CanUseGCD && (ActionManagerHelper.GetCurrentAnimationLock() == 0) && !Player.Object.IsCasting && (DataCenter.DefaultGCDElapsed >= DataCenter.DefaultGCDRemain); - public static bool FirstAbilityv2 => DataCenter.InCombat && !ActionHelper.CanUseGCD && (ActionManagerHelper.GetCurrentAnimationLock() == 0) && !Player.Object.IsCasting && (DataCenter.DefaultGCDRemain >= DataCenter.DefaultGCDElapsed); + /// + /// Calculates the action ahead time based on the default GCD total and minimum animation lock. + /// + public static float CalculatedActionAhead => Math.Min(DefaultGCDTotal * 0.20f, MinAnimationLock); - public static bool LastAbilityorNot => DataCenter.InCombat && (DataCenter.NextAbilityToNextGCD <= Math.Max(ActionManagerHelper.GetCurrentAnimationLock(), DataCenter.MinAnimationLock) + Service.Config.IsLastAbilityTimer); - public static bool FirstAbilityorNot => DataCenter.InCombat && (DataCenter.NextAbilityToNextGCD >= Math.Max(ActionManagerHelper.GetCurrentAnimationLock(), DataCenter.MinAnimationLock) + Service.Config.IsFirstAbilityTimer); + /// + /// Calculates the total GCD time for a given number of GCDs and an optional offset. + /// + /// The number of GCDs. + /// The optional offset. + /// The total GCD time. + public static float GCDTime(uint gcdCount = 0, float offset = 0) => DefaultGCDTotal * gcdCount + offset; #endregion + public static uint[] BluSlots { get; internal set; } = new uint[24]; public static uint[] DutyActions { get; internal set; } = new uint[2]; diff --git a/RotationSolver.Basic/Helpers/StatusHelper.cs b/RotationSolver.Basic/Helpers/StatusHelper.cs index 4c1ab3e9f..063af48de 100644 --- a/RotationSolver.Basic/Helpers/StatusHelper.cs +++ b/RotationSolver.Basic/Helpers/StatusHelper.cs @@ -206,12 +206,19 @@ internal static IEnumerable StatusTimes(this IGameObject obj, bool isFrom { if (obj == null) { - // Log or handle the case where obj is null PluginLog.Error("IGameObject is null. Cannot get status times."); return Enumerable.Empty(); } - return obj.GetStatus(isFromSelf, statusIDs).Select(status => status.RemainingTime == 0 ? float.MaxValue : status.RemainingTime); + var statuses = obj.GetStatus(isFromSelf, statusIDs); + var result = new List(); + + foreach (var status in statuses) + { + result.Add(status.RemainingTime == 0 ? float.MaxValue : status.RemainingTime); + } + + return result; } /// @@ -240,12 +247,19 @@ private static IEnumerable StatusStacks(this IGameObject obj, bool isFromS { if (obj == null) { - // Log or handle the case where obj is null PluginLog.Error("IGameObject is null. Cannot get status stacks."); return Enumerable.Empty(); } - return obj.GetStatus(isFromSelf, statusIDs).Select(status => status.StackCount == 0 ? byte.MaxValue : status.StackCount); + var statuses = obj.GetStatus(isFromSelf, statusIDs); + var result = new List(); + + foreach (var status in statuses) + { + result.Add(status.StackCount == 0 ? byte.MaxValue : status.StackCount); + } + + return result; } /// @@ -323,10 +337,19 @@ internal static string GetStatusName(StatusID id) /// An enumerable of statuses. private static IEnumerable GetStatus(this IGameObject obj, bool isFromSelf, params StatusID[] statusIDs) { - // Convert statusIDs to a HashSet for faster lookups var newEffects = new HashSet(statusIDs.Select(a => (uint)a)); var allStatuses = obj.GetAllStatus(isFromSelf); - return allStatuses.Where(status => newEffects.Contains(status.StatusId)); + var result = new List(); + + foreach (var status in allStatuses) + { + if (newEffects.Contains(status.StatusId)) + { + result.Add(status); + } + } + + return result; } /// @@ -340,24 +363,28 @@ private static IEnumerable GetAllStatus(this IGameObject obj, bool isFro if (obj is not IBattleChara b) return Enumerable.Empty(); var playerId = Player.Object?.GameObjectId ?? 0; + var result = new List(); try { - // Ensure b.StatusList is not null if (b.StatusList == null) { PluginLog.Error("StatusList is null. Cannot get statuses."); return Enumerable.Empty(); } - return b.StatusList.Where(status => !isFromSelf - || status.SourceId == playerId - || status.SourceObject?.OwnerId == playerId) - ?? Enumerable.Empty(); + foreach (var status in b.StatusList) + { + if (!isFromSelf || status.SourceId == playerId || status.SourceObject?.OwnerId == playerId) + { + result.Add(status); + } + } + + return result; } catch (Exception ex) { - // Log the exception Svc.Log.Error($"Failed to get statuses: {ex.Message}"); return Enumerable.Empty(); } diff --git a/RotationSolver.Basic/Helpers/TargetFilter.cs b/RotationSolver.Basic/Helpers/TargetFilter.cs index b5b75964a..4937db902 100644 --- a/RotationSolver.Basic/Helpers/TargetFilter.cs +++ b/RotationSolver.Basic/Helpers/TargetFilter.cs @@ -1,6 +1,5 @@ using ECommons.ExcelServices; using Lumina.Excel.Sheets; -using System.Data; namespace RotationSolver.Basic.Helpers; @@ -19,13 +18,15 @@ public static IEnumerable GetDeath(this IEnumerable { if (charas == null) return Enumerable.Empty(); - return charas.Where(item => + var result = new List(); + foreach (var item in charas) { - if (item == null || !item.IsDead || item.CurrentHp != 0 || !item.IsTargetable) return false; - if (item.HasStatus(false, StatusID.Raise)) return false; - if (!Service.Config.RaiseBrinkOfDeath && item.HasStatus(false, StatusID.BrinkOfDeath)) return false; - return true; - }); + if (item == null || !item.IsDead || item.CurrentHp != 0 || !item.IsTargetable) continue; + if (item.HasStatus(false, StatusID.Raise)) continue; + if (!Service.Config.RaiseBrinkOfDeath && item.HasStatus(false, StatusID.BrinkOfDeath)) continue; + result.Add(item); + } + return result; } /// @@ -38,11 +39,30 @@ public static IEnumerable GetJobCategory(this IEnumerable(); - var validJobs = new HashSet(roles.SelectMany(role => Service.GetSheet() - .Where(job => role == job.GetJobRole()) - .Select(job => (byte)job.RowId))); + var validJobs = new HashSet(); + var classJobs = Service.GetSheet(); - return objects.Where(obj => obj.IsJobs(validJobs)); + foreach (var role in roles) + { + foreach (var job in classJobs) + { + if (role == job.GetJobRole()) + { + validJobs.Add((byte)job.RowId); + } + } + } + + var result = new List(); + foreach (var obj in objects) + { + if (obj.IsJobs(validJobs)) + { + result.Add(obj); + } + } + + return result; } /// @@ -55,9 +75,16 @@ public static bool IsJobCategory(this IGameObject obj, JobRole role) { if (obj == null) return false; - var validJobs = new HashSet(Service.GetSheet() - .Where(job => role == job.GetJobRole()) - .Select(job => (byte)job.RowId)); + var validJobs = new HashSet(); + var classJobs = Service.GetSheet(); + + foreach (var job in classJobs) + { + if (role == job.GetJobRole()) + { + validJobs.Add((byte)job.RowId); + } + } return obj.IsJobs(validJobs); } @@ -72,7 +99,13 @@ public static bool IsJobs(this IGameObject obj, params Job[] validJobs) { if (obj == null || validJobs == null || validJobs.Length == 0) return false; - return obj.IsJobs(new HashSet(validJobs.Select(j => (byte)(uint)j))); + var validJobSet = new HashSet(); + foreach (var job in validJobs) + { + validJobSet.Add((byte)(uint)job); + } + + return obj.IsJobs(validJobSet); } private static bool IsJobs(this IGameObject obj, HashSet validJobs) @@ -93,6 +126,14 @@ public static IEnumerable GetObjectInRadius(this IEnumerable objects, f { if (objects == null) return Enumerable.Empty(); - return objects.Where(o => o.DistanceToPlayer() <= radius); + var result = new List(); + foreach (var obj in objects) + { + if (obj.DistanceToPlayer() <= radius) + { + result.Add(obj); + } + } + return result; } } \ No newline at end of file diff --git a/RotationSolver.Basic/Rotations/CustomRotation_Ability.cs b/RotationSolver.Basic/Rotations/CustomRotation_Ability.cs index 55e853131..0a540dfb9 100644 --- a/RotationSolver.Basic/Rotations/CustomRotation_Ability.cs +++ b/RotationSolver.Basic/Rotations/CustomRotation_Ability.cs @@ -410,7 +410,7 @@ protected virtual bool EmergencyAbility(IAction nextGCD, out IAction? act) if (nextGCD is BaseAction action) { if (Role is JobRole.RangedMagical && - action.Info.CastTime >= 5 && SwiftcastPvE.CanUse(out act, isFirstAbility: true)) + action.Info.CastTime >= 5 && IActionHelper.IsLastActionGCD() && SwiftcastPvE.CanUse(out act)) { return true; } @@ -418,7 +418,7 @@ protected virtual bool EmergencyAbility(IAction nextGCD, out IAction? act) if (DataCenter.CommandStatus.HasFlag(AutoStatus.Raise)) { - if (Role is JobRole.Healer && nextGCD.IsTheSameTo(true, ActionID.RaisePvE, ActionID.EgeiroPvE, ActionID.ResurrectionPvE, ActionID.AscendPvE) && SwiftcastPvE.CanUse(out act)) + if (Role is JobRole.Healer && IActionHelper.IsLastActionGCD() && (DataCenter.DefaultGCDRemain > Service.Config.SwiftcastBuffer) && nextGCD.IsTheSameTo(true, ActionID.RaisePvE, ActionID.EgeiroPvE, ActionID.ResurrectionPvE, ActionID.AscendPvE) && SwiftcastPvE.CanUse(out act)) { return true; } diff --git a/RotationSolver.Basic/Rotations/CustomRotation_GCD.cs b/RotationSolver.Basic/Rotations/CustomRotation_GCD.cs index 773396c65..48fe62035 100644 --- a/RotationSolver.Basic/Rotations/CustomRotation_GCD.cs +++ b/RotationSolver.Basic/Rotations/CustomRotation_GCD.cs @@ -113,9 +113,18 @@ partial class CustomRotation { IBaseAction.TargetOverride = TargetType.Heal; - if (DataCenter.PartyMembersDifferHP < Service.Config.HealthDifference - && DataCenter.PartyMembersHP.Count(i => i < 1) > 2 - && HealAreaGCD(out act)) return act; + if (DataCenter.PartyMembersDifferHP < Service.Config.HealthDifference) + { + int count = 0; + foreach (var hp in DataCenter.PartyMembersHP) + { + if (hp < 1) + { + count++; + } + } + if (count > 2 && HealAreaGCD(out act)) return act; + } if (HealSingleGCD(out act)) return act; IBaseAction.TargetOverride = null; @@ -139,18 +148,18 @@ partial class CustomRotation } - private bool UseLimitBreak(out IAction? act) - { - act = null; + //private bool UseLimitBreak(out IAction? act) + //{ + // act = null; - return LimitBreakLevel switch - { - 1 => (DataCenter.IsPvP ? LimitBreakPvP?.CanUse(out act, skipAoeCheck: true) : LimitBreak1?.CanUse(out act, skipAoeCheck: true)) ?? false, - 2 => LimitBreak2?.CanUse(out act, skipAoeCheck: true) ?? false, - 3 => LimitBreak3?.CanUse(out act, skipAoeCheck: true) ?? false, - _ => false, - }; - } + // return LimitBreakLevel switch + // { + // 1 => (DataCenter.IsPvP ? LimitBreakPvP?.CanUse(out act, skipAoeCheck: true) : LimitBreak1?.CanUse(out act, skipAoeCheck: true)) ?? false, + // 2 => LimitBreak2?.CanUse(out act, skipAoeCheck: true) ?? false, + // 3 => LimitBreak3?.CanUse(out act, skipAoeCheck: true) ?? false, + // _ => false, + // }; + //} private bool RaiseSpell(out IAction? act, bool mustUse) { @@ -169,7 +178,7 @@ private bool RaiseSpell(out IAction? act, bool mustUse) if (RaiseAction(out act, true)) { - if (HasSwift) return true; + if (HasSwift || IsLastAction(ActionID.SwiftcastPvE)) return true; if (Service.Config.RaisePlayerBySwift && !SwiftcastPvE.Cooldown.IsCoolingDown && SwiftcastPvE.CanUse(out act)) { @@ -291,6 +300,8 @@ protected virtual bool MoveForwardGCD(out IAction? act) [RotationDesc(DescType.HealSingleGCD)] protected virtual bool HealSingleGCD(out IAction? act) { + act = null; + if (ShouldSkipAction()) return false; if (DataCenter.CommandStatus.HasFlag(AutoStatus.HealSingleSpell)) { IBaseAction.ShouldEndSpecial = true; @@ -369,20 +380,18 @@ protected virtual bool DefenseAreaGCD(out IAction? act) protected virtual bool GeneralGCD(out IAction? act) { act = null; - + if (ShouldSkipAction()) return false; if (DataCenter.MergedStatus.HasFlag(AutoStatus.NoCasting)) { return false; } - if (ShouldSkipAction()) return false; - if (DataCenter.RightNowDutyRotation?.GeneralGCD(out act) ?? false) return true; act = null; return false; } private bool ShouldSkipAction() { - return DataCenter.CommandStatus.HasFlag(AutoStatus.Raise) && Role is JobRole.Healer && HasSwift; + return DataCenter.CommandStatus.HasFlag(AutoStatus.Raise) && Role is JobRole.Healer && (HasSwift || IsLastAction(ActionID.SwiftcastPvE)); } } diff --git a/RotationSolver/UI/RotationConfigWindow.cs b/RotationSolver/UI/RotationConfigWindow.cs index f57eb160c..3fef4a70d 100644 --- a/RotationSolver/UI/RotationConfigWindow.cs +++ b/RotationSolver/UI/RotationConfigWindow.cs @@ -2952,10 +2952,6 @@ private static void DrawNextAction() ImGui.Text(DataCenter.SpecialType.ToString()); ImGui.Text(ActionUpdater.NextAction?.Name ?? "null"); - ImGui.Text($"LastAbilityorNot: {DataCenter.LastAbilityorNot}"); - ImGui.Text($"FirstAbilityorNot: {DataCenter.FirstAbilityorNot}"); - ImGui.Text($"LastAbilityv2: {DataCenter.LastAbilityv2}"); - ImGui.Text($"FirstAbilityv2: {DataCenter.FirstAbilityv2}"); ImGui.Text($"GCD Total: {DataCenter.DefaultGCDTotal}"); ImGui.Text($"GCD Remain: {DataCenter.DefaultGCDRemain}"); ImGui.Text($"GCD Elapsed: {DataCenter.DefaultGCDElapsed}"); @@ -2990,32 +2986,6 @@ private static void DrawGCDCooldownStuff() ImGui.Text($"GCD Remain: {DataCenter.DefaultGCDRemain}"); ImGui.Text($"GCD Total: {DataCenter.DefaultGCDTotal}"); - // Change text color based on LastAbilityv2 - if (DataCenter.LastAbilityv2) - { - ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.ParsedGreen); - ImGui.Text("LastAbilityv2: true"); - } - else - { - ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGui.Text("LastAbilityv2: false"); - } - ImGui.PopStyleColor(); - - // Change text color based on FirstAbilityv2 - if (DataCenter.FirstAbilityv2) - { - ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.ParsedGreen); - ImGui.Text("FirstAbilityv2: true"); - } - else - { - ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGui.Text("FirstAbilityv2: false"); - } - ImGui.PopStyleColor(); - // Visualize the GCD and oGCD slots float gcdTotal = DataCenter.DefaultGCDElapsed + DataCenter.DefaultGCDRemain; float gcdProgress = DataCenter.DefaultGCDElapsed / gcdTotal; diff --git a/RotationSolver/Updaters/MajorUpdater.cs b/RotationSolver/Updaters/MajorUpdater.cs index 8e1014bbd..2543f5e49 100644 --- a/RotationSolver/Updaters/MajorUpdater.cs +++ b/RotationSolver/Updaters/MajorUpdater.cs @@ -186,18 +186,21 @@ private static void UpdateWork() private static void RemoveExpiredVfxData() { var expiredVfx = new List(); - for (int i = 0; i < DataCenter.VfxDataQueue.Count; i++) + lock (DataCenter.VfxDataQueue) { - var vfx = DataCenter.VfxDataQueue[i]; - if (vfx.TimeDuration > TimeSpan.FromSeconds(10)) + for (int i = 0; i < DataCenter.VfxDataQueue.Count; i++) { - expiredVfx.Add(vfx); + var vfx = DataCenter.VfxDataQueue[i]; + if (vfx.TimeDuration > TimeSpan.FromSeconds(10)) + { + expiredVfx.Add(vfx); + } } - } - foreach (var vfx in expiredVfx) - { - DataCenter.VfxDataQueue.Remove(vfx); + foreach (var vfx in expiredVfx) + { + DataCenter.VfxDataQueue.Remove(vfx); + } } } diff --git a/RotationSolver/Updaters/TargetUpdater.cs b/RotationSolver/Updaters/TargetUpdater.cs index 3c3220853..2b717f889 100644 --- a/RotationSolver/Updaters/TargetUpdater.cs +++ b/RotationSolver/Updaters/TargetUpdater.cs @@ -194,15 +194,30 @@ private static List GetAllHostileTargets() var deathAll = DataCenter.AllTargets?.GetDeath().ToList() ?? new List(); var deathNPC = DataCenter.FriendlyNPCMembers?.GetDeath().ToList() ?? new List(); var deathAllianceMembers = DataCenter.AllianceMembers?.GetDeath().ToList() ?? new List(); - var deathAllianceHealers = DataCenter.AllianceMembers?.Where(member => member.IsJobCategory(JobRole.Healer)) - .GetDeath().ToList() ?? new List(); - var deathAllianceSupports = DataCenter.AllianceMembers? - .Where(member => member.IsJobCategory(JobRole.Healer) || member.IsJobCategory(JobRole.Tank)) - .GetDeath() - .ToList() ?? new List(); - - var raisePartyAndAllianceSupports = deathParty.Concat(deathAllianceSupports).ToList(); - var raisePartyAndAllianceHealers = deathParty.Concat(deathAllianceHealers).ToList(); + var deathAllianceHealers = new List(); + var deathAllianceSupports = new List(); + + if (DataCenter.AllianceMembers != null) + { + foreach (var member in DataCenter.AllianceMembers) + { + if (member.IsJobCategory(JobRole.Healer)) + { + deathAllianceHealers.Add(member); + } + if (member.IsJobCategory(JobRole.Healer) || member.IsJobCategory(JobRole.Tank)) + { + deathAllianceSupports.Add(member); + } + } + } + + var raisePartyAndAllianceSupports = new List(deathParty); + raisePartyAndAllianceSupports.AddRange(deathAllianceSupports); + + var raisePartyAndAllianceHealers = new List(deathParty); + raisePartyAndAllianceHealers.AddRange(deathAllianceHealers); + var raisetype = Service.Config.RaiseType; var validRaiseTargets = new List(); @@ -229,7 +244,7 @@ private static List GetAllHostileTargets() validRaiseTargets.AddRange(deathNPC); } - foreach (var type in Enum.GetValues(typeof(RaiseType)).Cast()) + foreach (RaiseType type in Enum.GetValues(typeof(RaiseType))) { var deathTarget = GetPriorityDeathTarget(validRaiseTargets, type); if (deathTarget != null) return deathTarget; @@ -249,6 +264,7 @@ private static List GetAllHostileTargets() var deathTanks = new List(); var deathHealers = new List(); + var deathOffHealers = new List(); var deathOthers = new List(); foreach (var chara in validRaiseTargets) @@ -261,6 +277,10 @@ private static List GetAllHostileTargets() { deathHealers.Add(chara); } + else if (Service.Config.OffRaiserRaise && chara.IsJobs(Job.SMN, Job.RDM)) + { + deathOffHealers.Add(chara); + } else { deathOthers.Add(chara); @@ -272,9 +292,16 @@ private static List GetAllHostileTargets() return deathHealers[0]; } + if (Service.Config.H2) + { + deathOffHealers.Reverse(); + deathOthers.Reverse(); + } + if (deathTanks.Count > 1) return deathTanks[0]; if (deathHealers.Count > 0) return deathHealers[0]; if (deathTanks.Count > 0) return deathTanks[0]; + if (Service.Config.OffRaiserRaise && deathOffHealers.Count > 0) return deathOffHealers[0]; return deathOthers.Count > 0 ? deathOthers[0] : null; }