diff --git a/BasicRotations/Healer/SGE_Default.cs b/BasicRotations/Healer/SGE_Default.cs index e11fbf3e2..90dd8a9db 100644 --- a/BasicRotations/Healer/SGE_Default.cs +++ b/BasicRotations/Healer/SGE_Default.cs @@ -1,3 +1,5 @@ +using static FFXIVClientStructs.FFXIV.Client.UI.Misc.DataCenterHelper; + namespace RebornRotations.Healer; [Rotation("Default", CombatType.PvE, GameVersion = "7.15")] @@ -62,6 +64,10 @@ public sealed class SGE_Default : SageRotation #endregion + #region Tracking Properties + private IBaseAction? _lastEukrasiaActionAim = null; + #endregion + #region Countdown Logic protected override IAction? CountDownAction(float remainTime) { @@ -84,7 +90,24 @@ protected override bool AttackAbility(IAction nextGCD, out IAction? act) protected override bool EmergencyAbility(IAction nextGCD, out IAction? act) { - if (base.EmergencyAbility(nextGCD, out act)) return true; + if ((_EukrasiaActionAim == EukrasianDosisIiiPvE || _EukrasiaActionAim == EukrasianDosisIiPvE || _EukrasiaActionAim == EukrasianDosisPvE) + && (DyskrasiaPvE.CanUse(out _) || DyskrasiaIiPvE.CanUse(out _) || EukrasianDyskrasiaPvE.CanUse(out _))) + { + ClearEukrasia(); + } + + // If the last action performed matches any of a list of specific actions, it clears the Eukrasia aim. + // This serves as a reset/cleanup mechanism to ensure the decision logic starts fresh for the next cycle. + if (IsLastGCD(false, EukrasianPrognosisIiPvE, EukrasianPrognosisPvE, + EukrasianDiagnosisPvE, EukrasianDyskrasiaPvE, EukrasianDosisIiiPvE, EukrasianDosisIiPvE, + EukrasianDosisPvE) || !InCombat) + { + ClearEukrasia(); + } + + if (ChoiceEukrasia(out act)) return true; + + //if (base.EmergencyAbility(nextGCD, out act)) return true; if (nextGCD.IsTheSameTo(false, PneumaPvE, EukrasianPrognosisPvE, EukrasianPrognosisIiPvE)) { @@ -230,7 +253,7 @@ private void SetEukrasia(IBaseAction act) { if (act == null) return; - if (_EukrasiaActionAim != null && IsLastGCD(false, _EukrasiaActionAim)) return; + if (_EukrasiaActionAim != null && IsLastGCD(true, _EukrasiaActionAim)) return; if (_EukrasiaActionAim != act) { @@ -243,6 +266,7 @@ private void ClearEukrasia() { if (_EukrasiaActionAim != null) { + _lastEukrasiaActionAim = _EukrasiaActionAim; _EukrasiaActionAim = null; } } @@ -251,16 +275,8 @@ private bool ChoiceEukrasia(out IAction? act) { act = null; - // If the last action performed matches any of a list of specific actions, it clears the Eukrasia aim. - // This serves as a reset/cleanup mechanism to ensure the decision logic starts fresh for the next cycle. - if (IsLastGCD(true, EukrasianPrognosisIiPvE, EukrasianPrognosisPvE, - EukrasianDiagnosisPvE, EukrasianDyskrasiaPvE, EukrasianDosisIiiPvE, EukrasianDosisIiPvE, - EukrasianDosisPvE) || !InCombat) - { - ClearEukrasia(); - } - if (!EukrasiaPvE.CanUse(out _)) return false; + // Checks for Eukrasia status. // Attempts to set correct Eurkrasia action based on availablity and MergedStatus. if (EukrasianPrognosisIiPvE.CanUse(out _) && EukrasianPrognosisIiPvE.EnoughLevel && MergedStatus.HasFlag(AutoStatus.DefenseArea)) @@ -281,25 +297,25 @@ private bool ChoiceEukrasia(out IAction? act) return false; } - if (EukrasianDyskrasiaPvE.CanUse(out _) && EukrasianDyskrasiaPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseSingle))) + if (EukrasianDyskrasiaPvE.CanUse(out _) && EukrasianDyskrasiaPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseArea))) { SetEukrasia(EukrasianDyskrasiaPvE); return false; } - if (EukrasianDosisIiiPvE.CanUse(out _) && EukrasianDosisIiiPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseSingle))) + 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 (EukrasianDosisIiPvE.CanUse(out _) && EukrasianDosisIiPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseSingle))) + 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 (EukrasianDosisPvE.CanUse(out _) && EukrasianDosisPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseSingle))) + if ((!EukrasianDyskrasiaPvE.CanUse(out _) || !DyskrasiaPvE.CanUse(out _)) && EukrasianDosisPvE.CanUse(out _) && EukrasianDosisPvE.EnoughLevel && (!MergedStatus.HasFlag(AutoStatus.DefenseSingle) || !MergedStatus.HasFlag(AutoStatus.DefenseArea))) { SetEukrasia(EukrasianDosisPvE); return false; @@ -364,11 +380,8 @@ protected override bool HealSingleGCD(out IAction? act) protected override bool GeneralGCD(out IAction? act) { act = null; - if (HasSwift && SwiftLogic && EgeiroPvE.CanUse(out _)) return false; - if (PhlegmaIiiPvE.CanUse(out act, usedUp: IsMoving)) return true; - if (PhlegmaIiPvE.CanUse(out act, usedUp: IsMoving)) return true; 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)) @@ -378,10 +391,10 @@ protected override bool GeneralGCD(out IAction? act) if (IsMoving && ToxikonPvE.CanUse(out act)) return true; - if (ChoiceEukrasia(out act)) return true; - if (DoEukrasia(out act)) return true; + if ((_EukrasiaActionAim != EukrasianDiagnosisPvE || _EukrasiaActionAim != EukrasianPrognosisPvE || _EukrasiaActionAim != EukrasianPrognosisIiPvE || _EukrasiaActionAim != EukrasianDyskrasiaPvE) + && DyskrasiaPvE.CanUse(out act)) return true; - if (DyskrasiaPvE.CanUse(out act)) return true; + if (DoEukrasia(out act)) return true; if (DosisPvE.CanUse(out act)) return true; @@ -398,6 +411,7 @@ protected override bool GeneralGCD(out IAction? act) public override void DisplayStatus() { ImGui.Text($"Eukrasian Action: {_EukrasiaActionAim}"); + ImGui.Text($"Last Eukrasian Action: {_lastEukrasiaActionAim}"); ImGui.Text("HasEukrasia: " + HasEukrasia.ToString()); ImGui.Text("Addersgall: " + Addersgall.ToString()); ImGui.Text("Addersting: " + Addersting.ToString()); diff --git a/BasicRotations/Magical/RDM_Default.cs b/BasicRotations/Magical/RDM_Default.cs index f96d0453a..a48008678 100644 --- a/BasicRotations/Magical/RDM_Default.cs +++ b/BasicRotations/Magical/RDM_Default.cs @@ -106,13 +106,7 @@ protected override bool AttackAbility(IAction nextGCD, out IAction? act) ActionID.EnchantedMoulinetPvE, ActionID.MoulinetPvE //I dont know at this point if nextGCD.IsTheSameTo even working, but stil gonna left it in here. - }) && !nextGCD.IsTheSameTo(new[] - { - ActionID.RipostePvE, - ActionID.EnchantedRipostePvE, - ActionID.MoulinetPvE, - ActionID.EnchantedMoulinetPvE - }); + }) && !nextGCD.IsTheSameTo(true, ActionID.RipostePvE, ActionID.EnchantedRipostePvE, ActionID.MoulinetPvE, ActionID.EnchantedMoulinetPvE); //i really hate this. bool ambatumelee = Player.HasStatus(true, StatusID.Manafication, StatusID.MagickedSwordplay); diff --git a/BasicRotations/Melee/NIN_Default.cs b/BasicRotations/Melee/NIN_Default.cs index 657b4e0df..3cac7a1e1 100644 --- a/BasicRotations/Melee/NIN_Default.cs +++ b/BasicRotations/Melee/NIN_Default.cs @@ -317,6 +317,8 @@ protected override bool EmergencyAbility(IAction nextGCD, out IAction? act) // Initializes the action to null, indicating no action has been chosen yet. act = null; + if ((InCombat || (CommbatMudra && HasHostilesInMaxRange)) && ChoiceNinjutsu(out act)) return true; + // If Ninjutsu is available or not in combat, defers to the base class's emergency ability logic. if (!NoNinjutsu || !InCombat) return base.EmergencyAbility(nextGCD, out act); @@ -413,7 +415,6 @@ protected override bool GeneralGCD(out IAction? act) if (ForkedUse && ForkedRaijuPvE.CanUse(out act)) return true; } - if ((InCombat || (CommbatMudra && HasHostilesInMaxRange)) && ChoiceNinjutsu(out act)) return true; if (((!InCombat && CommbatMudra && HasHostilesInMaxRange) || !CombatElapsedLess(7)) && DoNinjutsu(out act)) return true; //No Ninjutsu diff --git a/ECommons b/ECommons index 55bc281dc..3de0ce6a4 160000 --- a/ECommons +++ b/ECommons @@ -1 +1 @@ -Subproject commit 55bc281dc97ccaff213dfa3f501e9f0c0ae9ee2f +Subproject commit 3de0ce6a4acb79e058a2ab09ccbc9113c8b15bfc diff --git a/RotationSolver.Basic/Actions/ActionBasicInfo.cs b/RotationSolver.Basic/Actions/ActionBasicInfo.cs index 219d569f9..7681addfd 100644 --- a/RotationSolver.Basic/Actions/ActionBasicInfo.cs +++ b/RotationSolver.Basic/Actions/ActionBasicInfo.cs @@ -29,6 +29,16 @@ public readonly struct ActionBasicInfo /// public readonly uint ID => _action.Action.RowId; + /// + /// The Range of the action. + /// + public readonly sbyte Range => _action.Action.Range; + + /// + /// The EffectRange of the action. + /// + public readonly byte EffectRange => _action.Action.EffectRange; + /// /// The icon of the action. /// diff --git a/RotationSolver.Basic/Actions/ActionTargetInfo.cs b/RotationSolver.Basic/Actions/ActionTargetInfo.cs index 3acfeb55a..cac1a8e1c 100644 --- a/RotationSolver.Basic/Actions/ActionTargetInfo.cs +++ b/RotationSolver.Basic/Actions/ActionTargetInfo.cs @@ -467,7 +467,7 @@ private bool CheckTimeToKill(IGameObject gameObject) { if (DataCenter.Territory?.ContentType == TerritoryContentType.Trials || (DataCenter.Territory?.ContentType == TerritoryContentType.Raids && - DataCenter.AllianceMembers.Count(p => p is IPlayerCharacter) == 8)) + DataCenter.AllianceMembers.Count(p => p is IPlayerCharacter) >= 8)) { var fallbackPoints = new[] { Vector3.Zero, new Vector3(100, 0, 100) }; var closestFallback = fallbackPoints.MinBy(p => Vector3.Distance(player.Position, p)); diff --git a/RotationSolver.Basic/Configuration/Configs.cs b/RotationSolver.Basic/Configuration/Configs.cs index a96da4f05..341395275 100644 --- a/RotationSolver.Basic/Configuration/Configs.cs +++ b/RotationSolver.Basic/Configuration/Configs.cs @@ -466,13 +466,13 @@ public const string Filter = HealingActionCondition, Section = 3)] private static readonly bool _healOutOfCombat = false; - [ConditionBool, UI("Heal solo instance NPCs", Description = "Experimental.", + [ConditionBool, UI("Heal solo instance NPCs (Only enable as needed)", Description = "Experimental.", Filter = HealingActionCondition, Section = 3)] private static readonly bool _friendlyBattleNPCHeal = false; - [ConditionBool, UI("Heal and raise Party NPCs.", Description = "Experimental, only enable as needed.", + [ConditionBool, UI("Heal and raise Party NPCs.", Filter = HealingActionCondition, Section = 3)] - private static readonly bool _friendlyPartyNPCHealRaise2 = false; + private static readonly bool _friendlyPartyNPCHealRaise2 = true; [ConditionBool, UI("Heal/Dance partner your chocobo", Description = "Experimental.", Filter = HealingActionCondition, Section = 3)] diff --git a/RotationSolver.Basic/Data/RaiseType.cs b/RotationSolver.Basic/Data/RaiseType.cs index a8cf2ac18..548faa82e 100644 --- a/RotationSolver.Basic/Data/RaiseType.cs +++ b/RotationSolver.Basic/Data/RaiseType.cs @@ -12,14 +12,20 @@ public enum RaiseType : byte PartyOnly, /// - /// Raise party and alliance members. + /// Raise party members and alliance supports. /// - [Description("Raise all.")] - PartyAndAlliance, + [Description("Raise party members and alliance supports.")] + PartyAndAllianceSupports, /// /// Raise party members and alliance healers. /// - [Description("Raise party members and non-party healers.")] + [Description("Raise party members and alliance healers.")] PartyAndAllianceHealers, + + /// + /// Raise all. + /// + [Description("Raise All.")] + All, } \ No newline at end of file diff --git a/RotationSolver.Basic/DataCenter.cs b/RotationSolver.Basic/DataCenter.cs index 45d633b2b..1692fdaeb 100644 --- a/RotationSolver.Basic/DataCenter.cs +++ b/RotationSolver.Basic/DataCenter.cs @@ -804,7 +804,10 @@ internal static void AddDamageRec(float damageRatio) public static bool IsCastingTankVfx() { - return IsCastingVfx(s => + // Create a copy of the VfxDataQueue to avoid modification during enumeration + var vfxDataQueueCopy = VfxDataQueue.ToList(); + + return IsCastingVfx(vfxDataQueueCopy, s => { if (!s.Path.StartsWith("vfx/lockon/eff/tank_lockon")) return false; if (!Player.Available) return false; @@ -816,20 +819,19 @@ public static bool IsCastingTankVfx() public static bool IsCastingAreaVfx() { - return IsCastingVfx(s => s.Path.StartsWith("vfx/lockon/eff/coshare")); + var vfxDataQueueCopy = VfxDataQueue.ToArray(); + return IsCastingVfx([.. vfxDataQueueCopy], s => s.Path.StartsWith("vfx/lockon/eff/coshare")); } - public static bool IsCastingVfx(Func isVfx) + public static bool IsCastingVfx(List vfxDataQueueCopy, Func isVfx) { // Ensure the list is not empty - if (VfxDataQueue.Count == 0) + if (vfxDataQueueCopy.Count == 0) { return false; } - // Create a copy of the VfxDataQueue to avoid modification during enumeration - var vfxDataQueueCopy = VfxDataQueue.ToList(); - + // Iterate over the copied list foreach (var vfx in vfxDataQueueCopy) { if (isVfx(vfx)) diff --git a/RotationSolver.Basic/Helpers/IActionHelper.cs b/RotationSolver.Basic/Helpers/IActionHelper.cs index 4eb8f1937..581b848b0 100644 --- a/RotationSolver.Basic/Helpers/IActionHelper.cs +++ b/RotationSolver.Basic/Helpers/IActionHelper.cs @@ -112,18 +112,19 @@ public static bool IsLastActionAbility() /// True if the action is the same as any of the provided actions, otherwise false. public static bool IsTheSameTo(this IAction action, bool isAdjust, params IAction[] actions) { - return actions != null && action.IsTheSameTo(GetIDFromActions(isAdjust, actions)); + return actions != null && action.IsTheSameTo(isAdjust, GetIDFromActions(isAdjust, actions)); } /// /// Determines if the action is the same as any of the provided action IDs. /// /// The action to check. + /// Whether to use the adjusted ID. /// The action IDs to check against. /// True if the action is the same as any of the provided action IDs, otherwise false. - public static bool IsTheSameTo(this IAction action, params ActionID[] actions) + public static bool IsTheSameTo(this IAction action, bool isAdjust, params ActionID[] actions) { - return action != null && actions != null && IsActionID((ActionID)action.AdjustedID, actions); + return action != null && actions != null && IsActionID(isAdjust ? (ActionID)action.AdjustedID : (ActionID)action.ID, actions); } /// diff --git a/RotationSolver.Basic/Helpers/ObjectHelper.cs b/RotationSolver.Basic/Helpers/ObjectHelper.cs index f07acbcd6..26e955dc9 100644 --- a/RotationSolver.Basic/Helpers/ObjectHelper.cs +++ b/RotationSolver.Basic/Helpers/ObjectHelper.cs @@ -11,7 +11,6 @@ using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Common.Component.BGCollision; using FFXIVClientStructs.FFXIV.Component.GUI; -using Lumina.Excel.Sheets; using RotationSolver.Basic.Configuration; using System.Collections.Concurrent; using System.Runtime.InteropServices; @@ -42,7 +41,7 @@ internal static bool CanProvoke(this IGameObject target) if (DataCenter.FateId != 0 && target.FateId() == DataCenter.FateId) return false; - //Removed the listed names. + // Removed the listed names. if (OtherConfiguration.NoProvokeNames.TryGetValue(Svc.ClientState.TerritoryType, out var ns1)) { foreach (var n in ns1) @@ -54,14 +53,13 @@ internal static bool CanProvoke(this IGameObject target) } } - //Target can move or too big and has a target + // Target can move or too big and has a target var targetNpc = target.GetObjectNPC(); if ((targetNpc?.Unknown0 == 0 || target.HitboxRadius >= 5) // Unknown12 used to be the flag checked for the mobs ability to move, honestly just guessing on this one && (target.TargetObject?.IsValid() ?? false)) { - //the target is not a tank role - if (Svc.Objects.SearchById(target.TargetObjectId) is IBattleChara battle - && !battle.IsJobCategory(JobRole.Tank) + // The target is not a tank role + if (Svc.Objects.SearchById(target.TargetObjectId) is IBattleChara targetObject && !targetObject.IsJobCategory(JobRole.Tank) && (Vector3.Distance(target.Position, Player.Object?.Position ?? Vector3.Zero) > 5)) { return true; @@ -83,7 +81,7 @@ internal static unsafe bool IsOthersPlayers(this IGameObject obj) internal static bool IsAttackable(this IBattleChara battleChara) { - if (battleChara.IsAllianceMember() == true) return false; + if (battleChara.IsAllianceMember()) return false; // Dead. if (Service.Config.FilterOneHpInvincible && battleChara.CurrentHp <= 1) return false; @@ -136,7 +134,7 @@ internal static bool IsAttackable(this IBattleChara battleChara) TargetHostileType.TargetsHaveTarget => battleChara.TargetObject is IBattleChara, TargetHostileType.AllTargetsWhenSolo => DataCenter.PartyMembers.Count < 2 || battleChara.TargetObject is IBattleChara, TargetHostileType.AllTargetsWhenSoloInDuty => (DataCenter.PartyMembers.Count < 2 && (Svc.Condition[ConditionFlag.BoundByDuty] || Svc.Condition[ConditionFlag.BoundByDuty56])) - || battleChara.TargetObject is IBattleChara, + || battleChara.TargetObject is IBattleChara, TargetHostileType.TargetIsInEnemiesList => battleChara.TargetObject is IBattleChara target && target.IsInEnemiesList(), TargetHostileType.AllTargetsWhenSoloTargetIsInEnemiesList => (DataCenter.PartyMembers.Count < 2 && (Svc.Condition[ConditionFlag.BoundByDuty] || Svc.Condition[ConditionFlag.BoundByDuty56])) || battleChara.TargetObject is IBattleChara target && target.IsInEnemiesList(), TargetHostileType.AllTargetsWhenSoloInDutyTargetIsInEnemiesList => DataCenter.PartyMembers.Count < 2 || battleChara.TargetObject is IBattleChara target && target.IsInEnemiesList(), @@ -167,17 +165,12 @@ internal static unsafe bool IsInEnemiesList(this IBattleChara battleChara) { var addons = Service.GetAddons(); - if (!addons.Any()) + if (addons == null || !addons.Any()) { return false; } - if (!addons.Any()) - { - return false; - } - - var addon = addons.FirstOrDefault(); + var addon = addons.First(); if (addon == IntPtr.Zero) { return false; @@ -247,36 +240,67 @@ internal static unsafe bool IsEnemy(this IGameObject obj) internal static unsafe bool IsAllianceMember(this IGameObject obj) => obj.GameObjectId is not 0 - && (!DataCenter.IsPvP && DataCenter.IsInAllianceRaid && obj is IPlayerCharacter - || DataCenter.IsInAllianceRaid && ActionManager.CanUseActionOnTarget((uint)ActionID.CurePvE, obj.Struct())); + && (!DataCenter.IsPvP && DataCenter.IsInAllianceRaid && obj is IPlayerCharacter && (ActionManager.CanUseActionOnTarget((uint)ActionID.RaisePvE, obj.Struct()) || ActionManager.CanUseActionOnTarget((uint)ActionID.CurePvE, obj.Struct()))); - private static readonly object _lock = new object(); + private static readonly object _lock = new(); internal static bool IsParty(this IGameObject gameObject) { if (gameObject == null) return false; - // Use a lock to ensure thread safety lock (_lock) { - // Accessing Player.Object and Svc.Party within the lock to ensure thread safety - if (gameObject.GameObjectId == Player.Object?.GameObjectId) return true; - foreach (var p in Svc.Party) + try { - if (p.GameObject?.GameObjectId == gameObject.GameObjectId) return true; + var playerObject = Player.Object; + if (playerObject == null) return false; + + if (gameObject.GameObjectId == playerObject.GameObjectId) return true; + + foreach (var p in Svc.Party) + { + if (p?.GameObject?.GameObjectId == gameObject.GameObjectId) return true; + } + + if (Service.Config.FriendlyPartyNpcHealRaise2 && gameObject.IsNpcPartyMember()) return true; + if (Service.Config.ChocoboPartyMember && gameObject.IsPlayerCharacterChocobo()) return true; + if (Service.Config.FriendlyBattleNpcHeal && gameObject.IsFriendlyBattleNPC()) return true; + if (Service.Config.FocusTargetIsParty && gameObject.IsFocusTarget() && gameObject.IsAllianceMember()) return true; + } + catch (Exception ex) + { + Svc.Log.Error($"Error in IsParty: {ex}"); + return false; } - if (Service.Config.FriendlyPartyNpcHealRaise2 && gameObject.GetBattleNPCSubKind() == BattleNpcSubKind.NpcPartyMember) return true; - if (Service.Config.ChocoboPartyMember && gameObject.GetNameplateKind() == NameplateKind.PlayerCharacterChocobo) return true; - if (Service.Config.FriendlyBattleNpcHeal && gameObject.GetNameplateKind() == NameplateKind.FriendlyBattleNPC) return true; - if (Service.Config.FocusTargetIsParty && gameObject.IsFocusTarget() == true && gameObject.IsAllianceMember() == true) return true; } return false; } - internal static bool IsFocusTarget(this IGameObject gameObject) + internal static bool IsNpcPartyMember(this IGameObject gameObj) + { + return gameObj?.GetBattleNPCSubKind() == BattleNpcSubKind.NpcPartyMember; + } + + internal static bool IsPlayerCharacterChocobo(this IGameObject gameObj) + { + return gameObj?.GetBattleNPCSubKind() == BattleNpcSubKind.Chocobo; + } + + internal static bool IsFriendlyBattleNPC(this IGameObject gameObj) + { + if (gameObj == null) + { + return false; + } + + var nameplateKind = gameObj?.GetNameplateKind(); + return nameplateKind == NameplateKind.FriendlyBattleNPC; + } + + internal static bool IsFocusTarget(this IGameObject gameObj) { var focusTarget = Svc.Targets.FocusTarget; - return focusTarget != null && focusTarget.GameObjectId == gameObject.GameObjectId; + return focusTarget != null && focusTarget.GameObjectId == gameObj.GameObjectId; } internal static bool IsTargetOnSelf(this IBattleChara IBattleChara) @@ -290,7 +314,7 @@ internal static bool IsDeathToRaise(this IGameObject obj) if (obj is IBattleChara b && b.CurrentHp != 0) return false; if (obj.HasStatus(false, StatusID.Raise)) return false; if (!Service.Config.RaiseBrinkOfDeath && obj.HasStatus(false, StatusID.BrinkOfDeath)) return false; - foreach (var c in DataCenter.AllianceMembers) + foreach (var c in DataCenter.PartyMembers) { if (c.CastTargetObjectId == obj.GameObjectId) return false; } @@ -308,8 +332,8 @@ internal static bool IsAlive(this IGameObject obj) /// /// /// - public static unsafe Dalamud.Game.ClientState.Objects.Enums.ObjectKind GetObjectKind(this IGameObject obj) - => (Dalamud.Game.ClientState.Objects.Enums.ObjectKind)obj.Struct()->ObjectKind; + public static unsafe ObjectKind GetObjectKind(this IGameObject obj) + => (ObjectKind)obj.Struct()->ObjectKind; /// /// Determines whether the specified game object is a top priority hostile target based on its name being listed. @@ -351,6 +375,7 @@ internal static bool IsTopPriorityHostile(this IGameObject obj) if (Player.Object == null) return false; // Check IBattleChara against the priority target list of OIDs if (PriorityTargetHelper.IsPriorityTarget(b.DataId)) return true; + // Ensure StatusList is not null before calling Any foreach (var status in b.StatusList) { @@ -358,9 +383,8 @@ internal static bool IsTopPriorityHostile(this IGameObject obj) } } - if (Service.Config.ChooseAttackMark && MarkingHelper.GetAttackSignTargets().FirstOrDefault(id => id != 0) == (long)obj.GameObjectId && obj.IsEnemy()) return true; + if (Service.Config.ChooseAttackMark && MarkingHelper.GetAttackSignTargets().Any(id => id != 0 && id == (long)obj.GameObjectId && obj.IsEnemy())) return true; - // Fate if (Service.Config.TargetFatePriority && fateId != 0 && obj.FateId() == fateId) return true; var icon = obj.GetNamePlateIcon(); @@ -392,7 +416,7 @@ internal static bool IsTopPriorityHostile(this IGameObject obj) internal static unsafe uint FateId(this IGameObject obj) => obj.Struct()->FateId; - static readonly ConcurrentDictionary _effectRangeCheck = new(); + static readonly ConcurrentDictionary _effectRangeCheck = []; /// /// Determines whether the specified game object can be interrupted. @@ -432,8 +456,8 @@ public static bool IsJeunoBossImmune(this IBattleChara obj) if (obj.IsEnemy()) { - if (obj.HasStatus(true, StatusID.EpicVillain) && - (player.HasStatus(true, StatusID.VauntedHero) || player.HasStatus(true, StatusID.FatedHero))) + if (obj.HasStatus(false, StatusID.EpicVillain) && + (player.HasStatus(false, StatusID.VauntedHero) || player.HasStatus(false, StatusID.FatedHero))) { if (Service.Config.InDebug) { @@ -442,8 +466,8 @@ public static bool IsJeunoBossImmune(this IBattleChara obj) return true; } - if (obj.HasStatus(true, StatusID.VauntedVillain) && - (player.HasStatus(true, StatusID.EpicHero) || player.HasStatus(true, StatusID.FatedHero))) + if (obj.HasStatus(false, StatusID.VauntedVillain) && + (player.HasStatus(false, StatusID.EpicHero) || player.HasStatus(false, StatusID.FatedHero))) { if (Service.Config.InDebug) { @@ -452,8 +476,8 @@ public static bool IsJeunoBossImmune(this IBattleChara obj) return true; } - if (obj.HasStatus(true, StatusID.FatedVillain) && - (player.HasStatus(true, StatusID.EpicHero) || player.HasStatus(true, StatusID.VauntedHero))) + if (obj.HasStatus(false, StatusID.FatedVillain) && + (player.HasStatus(false, StatusID.EpicHero) || player.HasStatus(false, StatusID.VauntedHero))) { if (Service.Config.InDebug) { @@ -606,7 +630,8 @@ internal static float TimeAlive(this IBattleChara b) _aliveStartTimes[b.GameObjectId] = DateTime.Now; } - return (float)(DateTime.Now - _aliveStartTimes[b.GameObjectId]).TotalSeconds; + var elapsedTime = (float)(DateTime.Now - _aliveStartTimes[b.GameObjectId]).TotalSeconds; + return elapsedTime > 30 ? 30 : elapsedTime; } private static readonly ConcurrentDictionary _deadStartTimes = new(); @@ -635,7 +660,8 @@ internal static float TimeDead(this IBattleChara b) _deadStartTimes[b.GameObjectId] = DateTime.Now; } - return (float)(DateTime.Now - _deadStartTimes[b.GameObjectId]).TotalSeconds; + var elapsedTime = (float)(DateTime.Now - _deadStartTimes[b.GameObjectId]).TotalSeconds; + return elapsedTime > 30 ? 30 : elapsedTime; } /// diff --git a/RotationSolver.Basic/Helpers/TargetFilter.cs b/RotationSolver.Basic/Helpers/TargetFilter.cs index 356360e60..b5b75964a 100644 --- a/RotationSolver.Basic/Helpers/TargetFilter.cs +++ b/RotationSolver.Basic/Helpers/TargetFilter.cs @@ -24,7 +24,6 @@ public static IEnumerable GetDeath(this IEnumerable 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; - if (DataCenter.AllianceMembers.Any(c => c.CastTargetObjectId == item.GameObjectId)) return false; return true; }); } diff --git a/RotationSolver.Basic/Rotations/Basic/SageRotation.cs b/RotationSolver.Basic/Rotations/Basic/SageRotation.cs index 61a9290db..02ca79acc 100644 --- a/RotationSolver.Basic/Rotations/Basic/SageRotation.cs +++ b/RotationSolver.Basic/Rotations/Basic/SageRotation.cs @@ -14,12 +14,12 @@ partial class SageRotation /// /// Gets the amount of Addersgall available. /// - public static byte Addersgall => JobGauge.Addersgall; + public static byte Addersgall => AddersgallTrait.EnoughLevel ? JobGauge.Addersgall : (byte)0; /// /// Gets the amount of Addersting available. /// - public static byte Addersting => JobGauge.Addersting; + public static byte Addersting => AdderstingTrait.EnoughLevel ? JobGauge.Addersting : (byte)0; static float AddersgallTimerRaw => JobGauge.AddersgallTimer / 1000f; @@ -27,7 +27,7 @@ partial class SageRotation /// Gets the amount of milliseconds elapsed until the next Addersgall is available. /// This counts from 0 to 20_000. /// - public static float AddersgallTime => AddersgallTimerRaw - DataCenter.DefaultGCDRemain; + public static float AddersgallTime => AddersgallTrait.EnoughLevel ? AddersgallTimerRaw - DataCenter.DefaultGCDRemain : 0; /// /// Used to determine if the cooldown for the next Addersgall will end within a specified time frame. @@ -71,7 +71,8 @@ static partial void ModifyDiagnosisPvE(ref ActionSetting setting) static partial void ModifyKardiaPvE(ref ActionSetting setting) { setting.TargetType = TargetType.Tank; - setting.ActionCheck = () => !DataCenter.AllianceMembers.Any(m => m.HasStatus(true, StatusID.Kardion)); + setting.TargetStatusProvide = [StatusID.Kardion]; + setting.ActionCheck = () => !DataCenter.PartyMembers.Any(m => m.HasStatus(true, StatusID.Kardion)); setting.CreateConfig = () => new ActionConfig() { TimeToKill = 0, diff --git a/RotationSolver.Basic/Rotations/CustomRotation_Ability.cs b/RotationSolver.Basic/Rotations/CustomRotation_Ability.cs index 0f9f711bf..a48c7e35c 100644 --- a/RotationSolver.Basic/Rotations/CustomRotation_Ability.cs +++ b/RotationSolver.Basic/Rotations/CustomRotation_Ability.cs @@ -1,3 +1,5 @@ +using RotationSolver.Basic.Helpers; + namespace RotationSolver.Basic.Rotations; partial class CustomRotation @@ -416,7 +418,7 @@ protected virtual bool EmergencyAbility(IAction nextGCD, out IAction? act) if (DataCenter.CommandStatus.HasFlag(AutoStatus.Raise)) { - if (Role is JobRole.Healer && SwiftcastPvE.CanUse(out act, isFirstAbility: true)) + if (Role is JobRole.Healer && nextGCD.IsTheSameTo(true, ActionID.RaisePvE, ActionID.EgeiroPvE, ActionID.ResurrectionPvE, ActionID.AscendPvE) && SwiftcastPvE.CanUse(out act)) { return true; } diff --git a/RotationSolver/UI/RotationConfigWindow.cs b/RotationSolver/UI/RotationConfigWindow.cs index 484a7e5a5..f57eb160c 100644 --- a/RotationSolver/UI/RotationConfigWindow.cs +++ b/RotationSolver/UI/RotationConfigWindow.cs @@ -1741,18 +1741,26 @@ static void DrawActionDebug() if (_activeAction is IBaseAction action) { - try { ImGui.Text("ID: " + action.Info.ID.ToString()); #if DEBUG ImGui.Text("Is Real GCD: " + action.Info.IsRealGCD.ToString()); - ImGui.Text("Resources: " + ActionManager.Instance()->CheckActionResources(ActionType.Action, action.AdjustedID).ToString()); - ImGui.Text("Status: " + ActionManager.Instance()->GetActionStatus(ActionType.Action, action.AdjustedID).ToString()); + + // Ensure ActionManager.Instance() is not null + if (ActionManager.Instance() != null) + { + ImGui.Text("Resources: " + ActionManager.Instance()->CheckActionResources(ActionType.Action, action.AdjustedID).ToString()); + ImGui.Text("Status: " + ActionManager.Instance()->GetActionStatus(ActionType.Action, action.AdjustedID).ToString()); + } + ImGui.Text("Cast Time: " + action.Info.CastTime.ToString()); ImGui.Text("MP: " + action.Info.MPNeed.ToString()); #endif ImGui.Text("AttackType: " + action.Info.AttackType.ToString()); + ImGui.Text("Level: " + action.Info.Level.ToString()); + ImGui.Text("Range: " + action.Info.Range.ToString()); + ImGui.Text("EffectRange: " + action.Info.EffectRange.ToString()); ImGui.Text("Aspect: " + action.Info.Aspect.ToString()); ImGui.Text("Has One:" + action.Cooldown.HasOneCharge.ToString()); ImGui.Text("Recast One: " + action.Cooldown.RecastTimeOneChargeRaw.ToString()); @@ -1764,19 +1772,24 @@ static void DrawActionDebug() ImGui.Text("Target Name: " + action.Target.Target?.Name ?? string.Empty); ImGui.Text($"SpellUnlocked: {action.Info.SpellUnlocked} ({action.Action.UnlockLink.RowId})"); } - catch + catch (Exception ex) { - + ImGui.TextColored(ImGuiColors.DalamudRed, "Error: " + ex.Message); } } else if (_activeAction is IBaseItem item) { try { - ImGui.Text("Status: " + ActionManager.Instance()->GetActionStatus(ActionType.Item, item.ID).ToString()); - ImGui.Text("Status HQ: " + ActionManager.Instance()->GetActionStatus(ActionType.Item, item.ID + 1000000).ToString()); - var remain = ActionManager.Instance()->GetRecastTime(ActionType.Item, item.ID) - ActionManager.Instance()->GetRecastTimeElapsed(ActionType.Item, item.ID); - ImGui.Text("remain: " + remain.ToString()); + // Ensure ActionManager.Instance() is not null + if (ActionManager.Instance() != null) + { + ImGui.Text("Status: " + ActionManager.Instance()->GetActionStatus(ActionType.Item, item.ID).ToString()); + ImGui.Text("Status HQ: " + ActionManager.Instance()->GetActionStatus(ActionType.Item, item.ID + 1000000).ToString()); + var remain = ActionManager.Instance()->GetRecastTime(ActionType.Item, item.ID) - ActionManager.Instance()->GetRecastTimeElapsed(ActionType.Item, item.ID); + ImGui.Text("remain: " + remain.ToString()); + } + ImGui.Text("CanUse: " + item.CanUse(out _, true).ToString()); if (item is HpPotionItem healPotionItem) @@ -1784,9 +1797,9 @@ static void DrawActionDebug() ImGui.Text("MaxHP:" + healPotionItem.MaxHp.ToString()); } } - catch + catch (Exception ex) { - + ImGui.TextColored(ImGuiColors.DalamudRed, "Error: " + ex.Message); } } } @@ -2775,7 +2788,7 @@ private static unsafe void DrawStatus() // Display all party members var partyMembers = DataCenter.PartyMembers; - if (partyMembers.Any()) + if (partyMembers.Count != 0) { ImGui.Text("Party Members:"); foreach (var member in partyMembers) @@ -2790,7 +2803,7 @@ private static unsafe void DrawStatus() // Display all party members var friendlyNPCMembers = DataCenter.FriendlyNPCMembers; - if (friendlyNPCMembers.Any()) + if (friendlyNPCMembers.Count != 0) { ImGui.Text("Friendly NPC Members:"); foreach (var member in friendlyNPCMembers) @@ -2878,6 +2891,9 @@ private static unsafe void DrawTargetData() ImGui.Text($"Is Boss Icon: {battleChara.IsBossFromIcon()}"); ImGui.Text($"Rank: {battleChara.GetObjectNPC()?.Rank.ToString() ?? string.Empty}"); ImGui.Text($"Has Positional: {battleChara.HasPositional()}"); + ImGui.Text($"IsNpcPartyMember: {battleChara.IsNpcPartyMember()}"); + ImGui.Text($"IsPlayerCharacterChocobo: {battleChara.IsPlayerCharacterChocobo()}"); + ImGui.Text($"IsFriendlyBattleNPC: {battleChara.IsFriendlyBattleNPC()}"); ImGui.Text($"Is Dying: {battleChara.IsDying()}"); ImGui.Text($"Is Alive: {battleChara.IsAlive()}"); ImGui.Text($"Is Party: {battleChara.IsParty()}"); diff --git a/RotationSolver/Updaters/TargetUpdater.cs b/RotationSolver/Updaters/TargetUpdater.cs index 8c0902f75..e9f8e9e1f 100644 --- a/RotationSolver/Updaters/TargetUpdater.cs +++ b/RotationSolver/Updaters/TargetUpdater.cs @@ -47,15 +47,20 @@ private static unsafe List GetPartyMembers() var partyMembers = new List(); try { - if (DataCenter.AllianceMembers != null) + if (DataCenter.PartyMembers != null) { - foreach (var member in DataCenter.AllianceMembers) + foreach (var member in DataCenter.AllTargets) { - if (ObjectHelper.IsParty(member) && member.Character() != null && - member.Character()->CharacterData.OnlineStatus != 15 && - member.Character()->CharacterData.OnlineStatus != 5 && member.IsTargetable) + if (member == null) continue; + + if (ObjectHelper.IsParty(member) && member.IsParty() && member.Character() != null) { - partyMembers.Add(member); + var character = member.Character(); + if (character != null && character->CharacterData.OnlineStatus != 15 && + character->CharacterData.OnlineStatus != 5 && member.IsTargetable) + { + partyMembers.Add(member); + } } } } @@ -72,11 +77,11 @@ private static unsafe List GetAllianceMembers() var allianceMembers = new List(); try { - if (DataCenter.AllTargets != null) + if (DataCenter.AllianceMembers != null) { foreach (var target in DataCenter.AllTargets) { - if (ObjectHelper.IsAllianceMember(target) && target.Character() != null && + if (ObjectHelper.IsAllianceMember(target) && !target.IsParty() && target.Character() != null && target.Character()->CharacterData.OnlineStatus != 15 && target.Character()->CharacterData.OnlineStatus != 5 && target.IsTargetable) { @@ -178,19 +183,68 @@ private static List GetAllHostileTargets() { try { - var deathAll = DataCenter.AllianceMembers?.GetDeath().ToList() ?? new List(); var deathParty = DataCenter.PartyMembers?.GetDeath().ToList() ?? new List(); + 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 raisetype = Service.Config.RaiseType; + + var validRaiseTargets = new List(); + + if (raisetype == RaiseType.PartyOnly) + { + validRaiseTargets.AddRange(deathParty); + } + + if (raisetype == RaiseType.PartyAndAllianceSupports) + { + validRaiseTargets.AddRange(raisePartyAndAllianceSupports); + } - var deathTarget = GetPriorityDeathTarget(deathParty); + if (raisetype == RaiseType.PartyAndAllianceHealers) + { + validRaiseTargets.AddRange(raisePartyAndAllianceHealers); + } + + if (raisetype == RaiseType.All) + { + validRaiseTargets.AddRange(deathAll); + } + + if (Service.Config.FriendlyPartyNpcHealRaise2) + { + validRaiseTargets.AddRange(deathNPC); + } + + if (Service.Config.FocusTargetIsParty) + { + validRaiseTargets.AddRange(deathNPC); + } + + var deathTarget = GetPriorityDeathTarget(validRaiseTargets, RaiseType.PartyOnly); + if (deathTarget != null) return deathTarget; + + deathTarget = GetPriorityDeathTarget(validRaiseTargets, RaiseType.PartyAndAllianceSupports); if (deathTarget != null) return deathTarget; - deathTarget = GetPriorityDeathTarget(deathAll, Service.Config.RaiseType); + deathTarget = GetPriorityDeathTarget(validRaiseTargets, RaiseType.PartyAndAllianceHealers); + if (deathTarget != null) return deathTarget; + + deathTarget = GetPriorityDeathTarget(validRaiseTargets, RaiseType.All); if (deathTarget != null) return deathTarget; if (Service.Config.FriendlyPartyNpcHealRaise2) { - deathTarget = GetPriorityDeathTarget(deathNPC); + deathTarget = GetPriorityDeathTarget(validRaiseTargets, Service.Config.RaiseType); if (deathTarget != null) return deathTarget; } } @@ -202,14 +256,15 @@ private static List GetAllHostileTargets() return null; } - private static IBattleChara? GetPriorityDeathTarget(List deathList, RaiseType raiseType = RaiseType.PartyOnly) + private static IBattleChara? GetPriorityDeathTarget(List validRaiseTargets, RaiseType raiseType = RaiseType.PartyOnly) { - if (deathList.Count == 0) return null; + if (validRaiseTargets.Count == 0) return null; var deathTanks = new List(); var deathHealers = new List(); + var deathOthers = new List(); - foreach (var chara in deathList) + foreach (var chara in validRaiseTargets) { if (chara.IsJobCategory(JobRole.Tank)) { @@ -219,18 +274,51 @@ private static List GetAllHostileTargets() { deathHealers.Add(chara); } + else + { + deathOthers.Add(chara); + } + } + + if (raiseType == RaiseType.PartyAndAllianceHealers) + { + foreach (var chara in validRaiseTargets) + { + if (chara.IsJobCategory(JobRole.Healer)) + { + deathHealers.Add(chara); + } + } + } + else if (raiseType == RaiseType.PartyAndAllianceSupports) + { + foreach (var chara in validRaiseTargets) + { + if (chara.IsJobCategory(JobRole.Tank)) + { + deathTanks.Add(chara); + } + else if (chara.IsJobCategory(JobRole.Healer)) + { + deathHealers.Add(chara); + } + else + { + deathOthers.Add(chara); + } + } } - if (raiseType == RaiseType.PartyAndAllianceHealers && deathHealers.Count > 0) + if (raiseType == RaiseType.PartyAndAllianceHealers) { - return deathHealers[0]; + if (deathHealers.Count > 0) return deathHealers[0]; } if (deathTanks.Count > 1) return deathTanks[0]; if (deathHealers.Count > 0) return deathHealers[0]; if (deathTanks.Count > 0) return deathTanks[0]; - return deathList[0]; + return deathOthers.Count > 0 ? deathOthers[0] : null; } private static IBattleChara? GetDispelTarget()