diff --git a/RotationSolver.Basic/Actions/ActionBasicInfo.cs b/RotationSolver.Basic/Actions/ActionBasicInfo.cs index abbd32273..9c4102e3e 100644 --- a/RotationSolver.Basic/Actions/ActionBasicInfo.cs +++ b/RotationSolver.Basic/Actions/ActionBasicInfo.cs @@ -149,83 +149,80 @@ public ActionBasicInfo(IBaseAction action, bool isDutyAction) internal readonly bool BasicCheck(bool skipStatusProvideCheck, bool skipComboCheck, bool skipCastingCheck) { - if (!_action.Config.IsEnabled || !IsOnSlot) return false; - + if (!IsActionEnabled() || !IsOnSlot) return false; if (IsLimitBreak) return true; + if (IsActionDisabled() || !EnoughLevel || !HasEnoughMP() || !IsQuestUnlocked()) return false; - //Disabled. - if (DataCenter.DisabledActionSequencer?.Contains(ID) ?? false) return false; + var player = Player.Object; - if (!EnoughLevel) return false; - if (DataCenter.CurrentMp < MPNeed) return false; - if (_action.Setting.UnlockedByQuestID != 0) - { - var isUnlockQuestComplete = QuestManager.IsQuestComplete(_action.Setting.UnlockedByQuestID); - if (!isUnlockQuestComplete) - { - var warning = $"The action {Name} is locked by the quest {_action.Setting.UnlockedByQuestID}. Please complete this quest to learn this action."; - WarningHelper.AddSystemWarning(warning); - return false; - } - } + if (IsStatusNeeded() || IsStatusProvided(skipStatusProvideCheck)) return false; + if (IsLimitBreakLevelLow() || !IsComboValid(skipComboCheck) || !IsRoleActionValid()) return false; + if (NeedsCasting(skipCastingCheck)) return false; + if (IsGeneralGCD && IsStatusProvidedDuringGCD()) return false; + if (!IsActionCheckValid() || !IsRotationCheckValid()) return false; - var player = Player.Object; + return true; + } - if (_action.Setting.StatusNeed != null) - { - if (player.WillStatusEndGCD(0, 0, - _action.Setting.StatusFromSelf, _action.Setting.StatusNeed)) return false; - } + private bool IsActionEnabled() => _action.Config.IsEnabled; - if (_action.Setting.StatusProvide != null && !skipStatusProvideCheck) - { - if (!player.WillStatusEndGCD(_action.Config.StatusGcdCount, 0, - _action.Setting.StatusFromSelf, _action.Setting.StatusProvide)) return false; - } + private bool IsActionDisabled() => DataCenter.DisabledActionSequencer?.Contains(ID) ?? false; - if (_action.Action.ActionCategory.Row == 15) - { - if (CustomRotation.LimitBreakLevel <= 1) return false; - } + private bool HasEnoughMP() => DataCenter.CurrentMp >= MPNeed; - if (!skipComboCheck && IsGeneralGCD) + private bool IsQuestUnlocked() + { + if (_action.Setting.UnlockedByQuestID == 0) return true; + var isUnlockQuestComplete = QuestManager.IsQuestComplete(_action.Setting.UnlockedByQuestID); + if (!isUnlockQuestComplete) { - if (!CheckForCombo()) return false; + var warning = $"The action {Name} is locked by the quest {_action.Setting.UnlockedByQuestID}. Please complete this quest to learn this action."; + WarningHelper.AddSystemWarning(warning); } + return isUnlockQuestComplete; + } - if (_action.Action.IsRoleAction) - { - if (!_action.Action.ClassJobCategory.Value?.DoesJobMatchCategory(DataCenter.Job) ?? false) return false; - } + private bool IsStatusNeeded() + { + var player = Player.Object; + return _action.Setting.StatusNeed != null && player.WillStatusEndGCD(0, 0, _action.Setting.StatusFromSelf, _action.Setting.StatusNeed); + } - //Need casting. - if (CastTime > 0 && !player.HasStatus(true, - [ - StatusID.Swiftcast, - StatusID.Triplecast, - StatusID.Dualcast, - ]) - && !ActionsNoNeedCasting.Contains(ID)) - { - //No casting. - if (DataCenter.SpecialType == SpecialCommandType.NoCasting) return false; + private bool IsStatusProvided(bool skipStatusProvideCheck) + { + var player = Player.Object; + return !skipStatusProvideCheck && _action.Setting.StatusProvide != null && !player.WillStatusEndGCD(_action.Config.StatusGcdCount, 0, _action.Setting.StatusFromSelf, _action.Setting.StatusProvide); + } - //Is knocking back. - if (DateTime.Now > DataCenter.KnockbackStart && DateTime.Now < DataCenter.KnockbackFinished) return false; + private bool IsLimitBreakLevelLow() => _action.Action.ActionCategory.Row == 15 && CustomRotation.LimitBreakLevel <= 1; - if (DataCenter.NoPoslock && DataCenter.IsMoving && !skipCastingCheck) return false; - } + private bool IsComboValid(bool skipComboCheck) => skipComboCheck || !IsGeneralGCD || CheckForCombo(); - if (IsGeneralGCD && _action.Setting.StatusProvide?.Length > 0 && _action.Setting.IsFriendly - && IActionHelper.IsLastGCD(true, _action) - && DataCenter.TimeSinceLastAction.TotalSeconds < 3) return false; + private bool IsRoleActionValid() + { + return !_action.Action.IsRoleAction || (_action.Action.ClassJobCategory.Value?.DoesJobMatchCategory(DataCenter.Job) ?? false); + } - if (!(_action.Setting.ActionCheck?.Invoke() ?? true)) return false; - if (!IBaseAction.ForceEnable && !(_action.Setting.RotationCheck?.Invoke() ?? true)) return false; + private bool IsRotationCheckValid() + { + return IBaseAction.ForceEnable || (_action.Setting.RotationCheck?.Invoke() ?? true); + } - return true; + private bool NeedsCasting(bool skipCastingCheck) + { + var player = Player.Object; + return CastTime > 0 && !player.HasStatus(true, new[] { StatusID.Swiftcast, StatusID.Triplecast, StatusID.Dualcast }) && !ActionsNoNeedCasting.Contains(ID) && + (DataCenter.SpecialType == SpecialCommandType.NoCasting || (DateTime.Now > DataCenter.KnockbackStart && DateTime.Now < DataCenter.KnockbackFinished) || + (DataCenter.NoPoslock && DataCenter.IsMoving && !skipCastingCheck)); } + private bool IsStatusProvidedDuringGCD() + { + return _action.Setting.StatusProvide?.Length > 0 && _action.Setting.IsFriendly && IActionHelper.IsLastGCD(true, _action) && DataCenter.TimeSinceLastAction.TotalSeconds < 3; + } + + private bool IsActionCheckValid() => _action.Setting.ActionCheck?.Invoke() ?? true; + private readonly bool CheckForCombo() { if (_action.Setting.ComboIdsNot != null) diff --git a/RotationSolver.Basic/Actions/ActionTargetInfo.cs b/RotationSolver.Basic/Actions/ActionTargetInfo.cs index 80551f70c..650e3b286 100644 --- a/RotationSolver.Basic/Actions/ActionTargetInfo.cs +++ b/RotationSolver.Basic/Actions/ActionTargetInfo.cs @@ -6,7 +6,6 @@ using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using RotationSolver.Basic.Configuration; -using System.Net.Mail; using static RotationSolver.Basic.Configuration.ConfigTypes; namespace RotationSolver.Basic.Actions; @@ -40,9 +39,18 @@ public struct ActionTargetInfo(IBaseAction action) /// Is this action friendly. /// public readonly bool IsTargetFriendly => action.Setting.IsFriendly; - #region Target Stuff - private readonly IEnumerable GetCanTargets(bool skipStatusProvideCheck, TargetType type) + #region Targetting Behaviour + + /// + /// Retrieves a collection of valid battle characters that can be targeted based on the specified criteria. + /// + /// If set to true, skips the status provide check. + /// The type of target to filter (e.g., Heal). + /// + /// An containing the valid targets. + /// + private IEnumerable GetCanTargets(bool skipStatusProvideCheck, TargetType type) { var allTargets = DataCenter.AllTargets; if (allTargets == null) return Enumerable.Empty(); @@ -64,43 +72,63 @@ private readonly IEnumerable GetCanTargets(bool skipStatusProvideC return validTargets.Where(b => isAuto || b.GameObjectId == targetObjectId || b.GameObjectId == playerObjectId) .Where(InViewTarget) .Where(CanUseTo) - .Where(action.Setting.CanTarget); + .Where(action.Setting.CanTarget) + .ToList(); } - private readonly List GetCanAffects(bool skipStatusProvideCheck, TargetType type) + /// + /// Retrieves a list of battle characters that can be affected based on the specified criteria. + /// + /// If set to true, skips the status provide check. + /// The type of target to filter (e.g., Heal). + /// + /// A containing the valid targets. + /// + private List GetCanAffects(bool skipStatusProvideCheck, TargetType type) { - if (EffectRange == 0) return []; + if (EffectRange == 0) return new List(); - var items = TargetFilter.GetObjectInRadius(action.Setting.IsFriendly + var targets = action.Setting.IsFriendly ? DataCenter.PartyMembers - : DataCenter.AllHostileTargets, - Range + EffectRange); + : DataCenter.AllHostileTargets; + + if (targets == null) return new List(); + + var items = TargetFilter.GetObjectInRadius(targets, Range + EffectRange); if (type == TargetType.Heal) { items = items.Where(i => i.GetHealthRatio() < 1); } - var objs = new List(items.Count()); + var validTargets = new List(items.Count()); foreach (var obj in items) { if (!GeneralCheck(obj, skipStatusProvideCheck)) continue; - objs.Add(obj); + validTargets.Add(obj); } - return objs; + return validTargets; } - private static bool InViewTarget(IBattleChara IGameObject) + /// + /// Determines whether the specified battle character is within the player's view and vision cone based on the configuration settings. + /// + /// The battle character to check. + /// + /// true if the battle character is within the player's view and vision cone; otherwise, false. + /// + private static bool InViewTarget(IBattleChara gameObject) { if (Service.Config.OnlyAttackInView) { - if (!Svc.GameGui.WorldToScreen(IGameObject.Position, out _)) return false; + if (!Svc.GameGui.WorldToScreen(gameObject.Position, out _)) return false; } - if (Service.Config.OnlyAttackInVisionCone) + + if (Service.Config.OnlyAttackInVisionCone && Player.Object != null) { - Vector3 dir = IGameObject.Position - Player.Object.Position; + Vector3 dir = gameObject.Position - Player.Object.Position; Vector2 dirVec = new(dir.Z, dir.X); double angle = Player.Object.GetFaceVector().AngleTo(dirVec); if (angle > Math.PI * Service.Config.AngleOfVisionCone / 360) @@ -108,14 +136,20 @@ private static bool InViewTarget(IBattleChara IGameObject) return false; } } + return true; } - private readonly unsafe bool CanUseTo(IGameObject tar) + /// + /// Determines whether the specified game object can be targeted and used for an action. + /// + /// The game object to check. + /// + /// true if the game object can be targeted and used for an action; otherwise, false. + /// + private unsafe bool CanUseTo(IGameObject tar) { - if (tar == null || !Player.Available) return false; - if (tar.GameObjectId == 0) return false; - if (tar.GameObjectId == 0) return false; + if (tar == null || !Player.Available || tar.GameObjectId == 0) return false; var tarAddress = tar.Struct(); if (tarAddress == null) return false; @@ -132,76 +166,100 @@ private readonly unsafe bool CanUseTo(IGameObject tar) ActionID.BishopAutoturretPvP, }; - private readonly bool IsSpecialAbility(uint iD) + private bool IsSpecialAbility(uint iD) { - if (_specialActions.Contains((ActionID)iD)) return true; - return false; + return _specialActions.Contains((ActionID)iD); } - private readonly bool GeneralCheck(IBattleChara IGameObject, bool skipStatusProvideCheck) + /// + /// Performs a general check on the specified battle character to determine if it meets the criteria for targeting. + /// + /// The battle character to check. + /// If set to true, skips the status provide check. + /// + /// true if the battle character meets the criteria for targeting; otherwise, false. + /// + private bool GeneralCheck(IBattleChara gameObject, bool skipStatusProvideCheck) { - if (!IGameObject.IsTargetable) return false; + if (!gameObject.IsTargetable) return false; - if (!Service.Config.TargetAllForFriendly - && IGameObject.IsAlliance() && !IGameObject.IsParty()) + if (!Service.Config.TargetAllForFriendly && gameObject.IsAlliance() && !gameObject.IsParty()) { return false; } - if (DataCenter.BlacklistedNameIds.Contains(IGameObject.NameId)) + if (DataCenter.BlacklistedNameIds.Contains(gameObject.NameId)) { return false; } - if (IGameObject.IsEnemy()) + if (gameObject.IsEnemy() && !gameObject.IsAttackable()) { - //Can't attack. - if (!IGameObject.IsAttackable()) return false; + return false; } - return CheckStatus(IGameObject, skipStatusProvideCheck) - && CheckTimeToKill(IGameObject) - && CheckResistance(IGameObject); + return CheckStatus(gameObject, skipStatusProvideCheck) + && CheckTimeToKill(gameObject) + && CheckResistance(gameObject); } - private readonly bool CheckStatus(IGameObject IGameObject, bool skipStatusProvideCheck) + /// + /// Checks the status of the specified game object to determine if it meets the criteria for the action. + /// + /// The game object to check. + /// If set to true, skips the status provide check. + /// + /// true if the game object meets the status criteria for the action; otherwise, false. + /// + private bool CheckStatus(IGameObject gameObject, bool skipStatusProvideCheck) { if (!action.Config.ShouldCheckStatus) return true; if (action.Setting.TargetStatusNeed != null) { - if (IGameObject.WillStatusEndGCD(0, 0, - action.Setting.StatusFromSelf, action.Setting.TargetStatusNeed)) return false; + if (gameObject.WillStatusEndGCD(0, 0, action.Setting.StatusFromSelf, action.Setting.TargetStatusNeed)) + { + return false; + } } if (action.Setting.TargetStatusProvide != null && !skipStatusProvideCheck) { - if (!IGameObject.WillStatusEndGCD(action.Config.StatusGcdCount, 0, - action.Setting.StatusFromSelf, action.Setting.TargetStatusProvide)) return false; + if (!gameObject.WillStatusEndGCD(action.Config.StatusGcdCount, 0, action.Setting.StatusFromSelf, action.Setting.TargetStatusProvide)) + { + return false; + } } return true; } - private readonly bool CheckResistance(IGameObject IGameObject) + /// + /// Checks the resistance status of the specified game object to determine if it meets the criteria for the action. + /// + /// The game object to check. + /// + /// true if the game object meets the resistance criteria for the action; otherwise, false. + /// + private bool CheckResistance(IGameObject gameObject) { if (action.Info.AttackType == AttackType.Magic) { - if (IGameObject.HasStatus(false, StatusHelper.MagicResistance)) + if (gameObject.HasStatus(false, StatusHelper.MagicResistance)) { return false; } } else if (action.Info.Aspect != Aspect.Piercing) // Physical { - if (IGameObject.HasStatus(false, StatusHelper.PhysicalResistance)) + if (gameObject.HasStatus(false, StatusHelper.PhysicalResistance)) { return false; } } if (Range >= 20) // Range { - if (IGameObject.HasStatus(false, StatusID.RangedResistance, StatusID.EnergyField)) + if (gameObject.HasStatus(false, StatusID.RangedResistance, StatusID.EnergyField)) { return false; } @@ -209,28 +267,47 @@ private readonly bool CheckResistance(IGameObject IGameObject) return true; } - private readonly bool CheckTimeToKill(IGameObject IGameObject) + /// + /// Checks the time to kill of the specified game object to determine if it meets the criteria for the action. + /// + /// The game object to check. + /// + /// true if the game object meets the time to kill criteria for the action; otherwise, false. + /// + private bool CheckTimeToKill(IGameObject gameObject) { - if (IGameObject is not IBattleChara b) return false; + if (gameObject is not IBattleChara b) return false; var time = b.GetTimeToKill(); return float.IsNaN(time) || time >= action.Config.TimeToKill; } #endregion + #region Target Result + /// - /// Take a little long time.. + /// Finds the target based on the specified criteria. /// - /// - internal readonly TargetResult? FindTarget(bool skipAoeCheck, bool skipStatusProvideCheck) + /// If set to true, skips the AoE check. + /// If set to true, skips the status provide check. + /// + /// A containing the target and affected characters, or null if no target is found. + /// + internal TargetResult? FindTarget(bool skipAoeCheck, bool skipStatusProvideCheck) { var range = Range; var player = Player.Object; + if (player == null) + { + return null; + } + if (range == 0 && EffectRange == 0) { - return new(player, [], player.Position); + return new TargetResult(player, Array.Empty(), player.Position); } + var type = action.Setting.TargetType; var canTargets = GetCanTargets(skipStatusProvideCheck, type); @@ -243,14 +320,25 @@ private readonly bool CheckTimeToKill(IGameObject IGameObject) var targets = GetMostCanTargetObjects(canTargets, canAffects, skipAoeCheck ? 0 : action.Config.AoeCount); var target = FindTargetByType(targets, type, action.Config.AutoHealRatio, action.Setting.SpecialType); - if (target == null) return null; - return new(target, [.. GetAffects(target, canAffects)], target.Position); + return target == null ? null : new TargetResult(target, GetAffects(target, canAffects).ToArray(), target.Position); } - private readonly TargetResult? FindTargetArea(IEnumerable canTargets, IEnumerable canAffects, + /// + /// Finds the target area based on the specified criteria. + /// + /// The potential targets that can be affected. + /// The potential characters that can be affected. + /// The range within which to find the target area. + /// The player character. + /// + /// A containing the target area and affected characters, or null if no target area is found. + /// + private TargetResult? FindTargetArea(IEnumerable canTargets, IEnumerable canAffects, float range, IPlayerCharacter player) { - if (action.Setting.TargetType is TargetType.Move) + if (canTargets == null || canAffects == null || player == null) return null; + + if (action.Setting.TargetType == TargetType.Move) { return FindTargetAreaMove(range); } @@ -267,80 +355,117 @@ private readonly bool CheckTimeToKill(IGameObject IGameObject) } } - - private readonly TargetResult? FindTargetAreaHostile(IEnumerable canTargets, IEnumerable canAffects, int aoeCount) + /// + /// Finds the hostile target area based on the specified criteria. + /// + /// The potential targets that can be affected. + /// The potential characters that can be affected. + /// The number of targets to consider for AoE. + /// + /// A containing the target and affected characters, or null if no target is found. + /// + private TargetResult? FindTargetAreaHostile(IEnumerable canTargets, IEnumerable canAffects, int aoeCount) { + if (canTargets == null || canAffects == null) return null; + var target = GetMostCanTargetObjects(canTargets, canAffects, aoeCount) .OrderByDescending(ObjectHelper.GetHealthRatio).FirstOrDefault(); + if (target == null) return null; - return new(target, [.. GetAffects(target, canAffects)], target.Position); + + return new TargetResult(target, GetAffects(target, canAffects).ToArray(), target.Position); } - private readonly TargetResult? FindTargetAreaMove(float range) + /// + /// Finds the target area for movement based on the specified range. + /// + /// The range within which to find the target area. + /// + /// A containing the target area and affected characters, or null if no target area is found. + /// + private TargetResult? FindTargetAreaMove(float range) { + var player = Player.Object; + if (player == null) return null; + if (Service.Config.MoveAreaActionFarthest) { - Vector3 pPosition = Player.Object.Position; - if (Service.Config.MoveTowardsScreenCenter) unsafe + Vector3 pPosition = player.Position; + if (Service.Config.MoveTowardsScreenCenter) + { + unsafe { - var camera = CameraManager.Instance()->CurrentCamera; + var cameraManager = CameraManager.Instance(); + if (cameraManager == null) return null; + + var camera = cameraManager->CurrentCamera; var tar = camera->LookAtVector - camera->Object.Position; tar.Y = 0; var length = ((Vector3)tar).Length(); if (length == 0) return null; + tar = tar / length * range; - return new(Player.Object, [], new Vector3(pPosition.X + tar.X, - pPosition.Y, pPosition.Z + tar.Z)); + return new TargetResult(player, Array.Empty(), new Vector3(pPosition.X + tar.X, pPosition.Y, pPosition.Z + tar.Z)); } + } else { - float rotation = Player.Object.Rotation; - return new(Player.Object, [], new Vector3(pPosition.X + (float)Math.Sin(rotation) * range, - pPosition.Y, pPosition.Z + (float)Math.Cos(rotation) * range)); + float rotation = player.Rotation; + return new TargetResult(player, Array.Empty(), new Vector3(pPosition.X + (float)Math.Sin(rotation) * range, pPosition.Y, pPosition.Z + (float)Math.Cos(rotation) * range)); } } else { - var availableCharas = DataCenter.AllTargets.Where(b => b.GameObjectId != Player.Object.GameObjectId); - var target = FindTargetByType(TargetFilter.GetObjectInRadius(availableCharas, range), - TargetType.Move, action.Config.AutoHealRatio, action.Setting.SpecialType); + var availableCharas = DataCenter.AllTargets.Where(b => b.GameObjectId != player.GameObjectId); + var target = FindTargetByType(TargetFilter.GetObjectInRadius(availableCharas, range), TargetType.Move, action.Config.AutoHealRatio, action.Setting.SpecialType); if (target == null) return null; - return new(target, [], target.Position); + + return new TargetResult(target, Array.Empty(), target.Position); } } - - private readonly TargetResult? FindTargetAreaFriend(float range, IEnumerable canAffects, IPlayerCharacter player) + /// + /// Finds the target area for friendly actions based on the specified range and strategy. + /// + /// The range within which to find the target area. + /// The potential characters that can be affected. + /// The player character. + /// + /// A containing the target area and affected characters, or null if no target area is found. + /// + private TargetResult? FindTargetAreaFriend(float range, IEnumerable canAffects, IPlayerCharacter player) { + if (canAffects == null || player == null) return null; + var strategy = Service.Config.BeneficialAreaStrategy; switch (strategy) { case BeneficialAreaStrategy.OnLocations: // Find from list case BeneficialAreaStrategy.OnlyOnLocations: // Only the list OtherConfiguration.BeneficialPositions.TryGetValue(Svc.ClientState.TerritoryType, out var pts); - - pts ??= []; + pts ??= Array.Empty(); if (pts.Length == 0) { if (DataCenter.TerritoryContentType == TerritoryContentType.Trials || - DataCenter.TerritoryContentType == TerritoryContentType.Raids - && DataCenter.AllianceMembers.Count(p => p is IPlayerCharacter) == 8) + DataCenter.TerritoryContentType == TerritoryContentType.Raids && + DataCenter.AllianceMembers.Count(p => p is IPlayerCharacter) == 8) { - pts = [.. pts, Vector3.Zero, new(100, 0, 100)]; + pts = pts.Concat(new[] { Vector3.Zero, new Vector3(100, 0, 100) }).ToArray(); } } if (pts.Length > 0) { var closest = pts.MinBy(p => Vector3.Distance(player.Position, p)); - var rotation = new Random().NextDouble() * Math.Tau; - var radius = new Random().NextDouble() * 1; + var random = new Random(); + var rotation = random.NextDouble() * Math.Tau; + var radius = random.NextDouble() * 1; closest.X += (float)(Math.Sin(rotation) * radius); closest.Z += (float)(Math.Cos(rotation) * radius); if (Vector3.Distance(player.Position, closest) < player.HitboxRadius + EffectRange) { - return new(player, [.. GetAffects(closest, canAffects)], closest); + return new TargetResult(player, GetAffects(closest, canAffects).ToArray(), closest); } } @@ -351,7 +476,7 @@ private readonly bool CheckTimeToKill(IGameObject IGameObject) if (Svc.Targets.Target != null && Svc.Targets.Target.DistanceToPlayer() < range) { var target = Svc.Targets.Target as IBattleChara; - return new(target, [.. GetAffects(target?.Position, canAffects)], target?.Position); + return new TargetResult(target, GetAffects(target?.Position, canAffects).ToArray(), target?.Position); } break; } @@ -359,7 +484,7 @@ private readonly bool CheckTimeToKill(IGameObject IGameObject) if (Svc.Targets.Target is IBattleChara b && b.DistanceToPlayer() < range && b.IsBossFromIcon() && b.HasPositional() && b.HitboxRadius <= 8) { - return new(b, [.. GetAffects(b.Position, canAffects)], b.Position); + return new TargetResult(b, GetAffects(b.Position, canAffects).ToArray(), b.Position); } else { @@ -369,7 +494,7 @@ private readonly bool CheckTimeToKill(IGameObject IGameObject) if (attackT == null) { - return new(player, [.. GetAffects(player.Position, canAffects)], player.Position); + return new TargetResult(player, GetAffects(player.Position, canAffects).ToArray(), player.Position); } else { @@ -378,21 +503,30 @@ private readonly bool CheckTimeToKill(IGameObject IGameObject) if (disToTankRound < effectRange || disToTankRound > 2 * effectRange - player.HitboxRadius) { - return new(player, [.. GetAffects(player.Position, canAffects)], player.Position); + return new TargetResult(player, GetAffects(player.Position, canAffects).ToArray(), player.Position); } else { Vector3 directionToTank = attackT.Position - player.Position; - var MoveDirection = directionToTank / directionToTank.Length() * Math.Max(0, disToTankRound - effectRange); - return new(player, [.. GetAffects(player.Position, canAffects)], player.Position + MoveDirection); + var moveDirection = directionToTank / directionToTank.Length() * Math.Max(0, disToTankRound - effectRange); + return new TargetResult(player, GetAffects(player.Position, canAffects).ToArray(), player.Position + moveDirection); } } } } - private readonly IEnumerable GetAffects(Vector3? point, IEnumerable canAffects) + /// + /// Gets the characters that are affected within the specified range from a given point. + /// + /// The point from which to measure the effect range. + /// The potential characters that can be affected. + /// + /// An containing the characters that are within the effect range. + /// + private IEnumerable GetAffects(Vector3? point, IEnumerable canAffects) { - if (point == null) yield break; + if (point == null || canAffects == null) yield break; + foreach (var t in canAffects) { if (Vector3.Distance(point.Value, t.Position) - t.HitboxRadius <= EffectRange) @@ -402,8 +536,18 @@ private readonly IEnumerable GetAffects(Vector3? point, IEnumerabl } } - private readonly IEnumerable GetAffects(IBattleChara tar, IEnumerable canAffects) + /// + /// Gets the characters that are affected by the specified target. + /// + /// The target character. + /// The potential characters that can be affected. + /// + /// An containing the characters that are affected by the target. + /// + private IEnumerable GetAffects(IBattleChara tar, IEnumerable canAffects) { + if (tar == null || canAffects == null) yield break; + foreach (var t in canAffects) { if (CanGetTarget(tar, t)) @@ -413,12 +557,32 @@ private readonly IEnumerable GetAffects(IBattleChara tar, IEnumera } } + #endregion + #region Get Most Target - private readonly IEnumerable GetMostCanTargetObjects(IEnumerable canTargets, IEnumerable canAffects, int aoeCount) + + /// + /// Gets the most targetable objects based on the specified criteria. + /// + /// The potential targets that can be affected. + /// The potential characters that can be affected. + /// The number of targets to consider for AoE. + /// + /// An containing the most targetable objects based on the specified criteria. + /// + private IEnumerable GetMostCanTargetObjects(IEnumerable canTargets, IEnumerable canAffects, int aoeCount) { - if (IsSingleTarget || EffectRange <= 0) return canTargets; - if (!action.Setting.IsFriendly && Service.Config.AoEType == AoEType.Off) return []; - if (aoeCount > 1 && Service.Config.AoEType == AoEType.Cleave) return []; + if (canTargets == null || canAffects == null) yield break; + if (IsSingleTarget || EffectRange <= 0) + { + foreach (var target in canTargets) + { + yield return target; + } + yield break; + } + if (!action.Setting.IsFriendly && Service.Config.AoEType == AoEType.Off) yield break; + if (aoeCount > 1 && Service.Config.AoEType == AoEType.Cleave) yield break; List objectMax = new(canTargets.Count()); @@ -437,25 +601,29 @@ private readonly IEnumerable GetMostCanTargetObjects(IEnumerable 0 && objectMax.Count > 0 && objectMax.Count < action.Config.AoeCount && !action.Setting.IsFriendly) - //{ - // return []; - //} - //else + + foreach (var obj in objectMax) { - return objectMax; + yield return obj; } } - private readonly int CanGetTargetCount(IGameObject target, IEnumerable canAffects) + /// + /// Counts the number of objects that can be targeted based on the specified criteria. + /// + /// The target object. + /// The potential objects that can be affected. + /// The count of objects that can be targeted. + private int CanGetTargetCount(IGameObject target, IEnumerable canAffects) { + if (target == null || canAffects == null) return 0; + int count = 0; foreach (var t in canAffects) { if (target != t && !CanGetTarget(target, t)) continue; - if (Service.Config.NoNewHostiles - && t.TargetObject == null) + if (Service.Config.NoNewHostiles && t.TargetObject == null) { return 0; } @@ -466,9 +634,15 @@ private readonly int CanGetTargetCount(IGameObject target, IEnumerable + /// Determines if the sub-target can be targeted based on the specified criteria. + /// + /// The main target object. + /// The sub-target object. + /// True if the sub-target can be targeted; otherwise, false. private readonly bool CanGetTarget(IGameObject target, IGameObject subTarget) { - if (target == null) return false; + if (target == null || subTarget == null) return false; var pPos = Player.Object.Position; Vector3 dir = target.Position - pPos; @@ -484,25 +658,27 @@ private readonly bool CanGetTarget(IGameObject target, IGameObject subTarget) tdir += dir / dir.Length() * target.HitboxRadius / (float)Math.Sin(_alpha); return Vector3.Dot(dir, tdir) / (dir.Length() * tdir.Length()) >= Math.Cos(_alpha); - case 4: //Line + case 4: // Line if (subTarget.DistanceToPlayer() > EffectRange) return false; - return Vector3.Cross(dir, tdir).Length() / dir.Length() <= 2 + target.HitboxRadius && Vector3.Dot(dir, tdir) >= 0; - case 10: //Donut + case 10: // Donut var dis = Vector3.Distance(target.Position, subTarget.Position) - subTarget.HitboxRadius; return dis <= EffectRange && dis >= 8; - } - Svc.Log.Debug(action.Action.Name.RawString + "'s CastType is not valid! The value is " + action.Action.CastType.ToString()); - return false; + default: + Svc.Log.Debug($"{action.Action.Name.RawString}'s CastType is not valid! The value is {action.Action.CastType}"); + return false; + } } #endregion #region TargetFind private static IBattleChara? FindTargetByType(IEnumerable IGameObjects, TargetType type, float healRatio, SpecialActionType actionType) { + if (IGameObjects == null) return null; + if (type == TargetType.Self) return Player.Object; switch (actionType) @@ -537,7 +713,7 @@ private readonly bool CanGetTarget(IGameObject target, IGameObject subTarget) break; } - return type switch //Find the object. + return type switch // Find the object. { TargetType.Provoke => FindProvokeTarget(), TargetType.Dispel => FindDispelTarget(), @@ -557,34 +733,43 @@ private readonly bool CanGetTarget(IGameObject target, IGameObject subTarget) IBattleChara? FindDancePartner() { - //DancePartnerPriority Based on the info from The Balance Discord for Level 100 - Job[] DancePartnerPriority = [Job.PCT, Job.SAM, Job.RPR, Job.VPR, Job.MNK, Job.NIN, Job.DRG, Job.BLM, Job.RDM, Job.SMN, Job.MCH, Job.BRD, Job.DNC]; + // DancePartnerPriority based on the info from The Balance Discord for Level 100 + Job[] DancePartnerPriority = { Job.PCT, Job.SAM, Job.RPR, Job.VPR, Job.MNK, Job.NIN, Job.DRG, Job.BLM, Job.RDM, Job.SMN, Job.MCH, Job.BRD, Job.DNC }; var PartyMembers = IGameObjects.Where(ObjectHelper.IsParty); foreach (var job in DancePartnerPriority) - foreach ( var member in PartyMembers) - if(member.IsJobs(job) && !member.IsDead) + { + foreach (var member in PartyMembers) + { + if (member.IsJobs(job) && !member.IsDead) + { return member; + } + } + } return RandomMeleeTarget(IGameObjects) ?? RandomRangeTarget(IGameObjects) ?? RandomMagicalTarget(IGameObjects) ?? RandomPhysicalTarget(IGameObjects) ?? null; - } IBattleChara? FindProvokeTarget() { if (IGameObjects.Any(o => o.GameObjectId == DataCenter.ProvokeTarget?.GameObjectId)) + { return DataCenter.ProvokeTarget; + } return null; } IBattleChara? FindDeathPeople() { if (IGameObjects.Any(o => o.GameObjectId == DataCenter.DeathTarget?.GameObjectId)) + { return DataCenter.DeathTarget; + } return null; } @@ -668,11 +853,15 @@ private readonly bool CanGetTarget(IGameObject target, IGameObject subTarget) var healerTar = healerTars.FirstOrDefault(); if (healerTar != null && healerTar.GetHealthRatio() < Service.Config.HealthHealerRatio) + { return healerTar; + } var tankTar = tankTars.FirstOrDefault(); if (tankTar != null && tankTar.GetHealthRatio() < Service.Config.HealthTankRatio) + { return tankTar; + } var tar = objs.FirstOrDefault(); if (tar?.GetHealthRatio() < 1) return tar; @@ -684,7 +873,9 @@ private readonly bool CanGetTarget(IGameObject target, IGameObject subTarget) IBattleChara? FindInterruptTarget() { if (IGameObjects.Any(o => o.GameObjectId == DataCenter.InterruptTarget?.GameObjectId)) + { return DataCenter.InterruptTarget; + } return null; } @@ -694,7 +885,7 @@ private readonly bool CanGetTarget(IGameObject target, IGameObject subTarget) if (Service.Config.FilterStopMark) { - var cs = MarkingHelper.FilterStopCharaes(IGameObjects); + var cs = MarkingHelper.FilterStopCharacters(IGameObjects); if (cs?.Any() ?? false) IGameObjects = cs; } @@ -718,10 +909,13 @@ private readonly bool CanGetTarget(IGameObject target, IGameObject subTarget) { if (DataCenter.IsPvP) { - return IGameObjects - .OfType() - .OrderBy(p => p.CurrentHp) - .FirstOrDefault(); + var orderedGameObjects = DataCenter.TargetingType switch + { + TargetingType.LowMaxHP => IGameObjects.OrderBy(p => p is IBattleChara b ? b.MaxHp : 0), + _ => IGameObjects.OrderBy(p => p is IBattleChara b ? b.MaxHp : 0), + }; + + return orderedGameObjects.OfType().FirstOrDefault(); } else { @@ -768,7 +962,9 @@ private readonly bool CanGetTarget(IGameObject target, IGameObject subTarget) IBattleChara? FindDispelTarget() { if (IGameObjects.Any(o => o.GameObjectId == DataCenter.DispelTarget?.GameObjectId)) + { return DataCenter.DispelTarget; + } return IGameObjects.FirstOrDefault(o => o is IBattleChara b && b.StatusList.Any(StatusHelper.CanDispel)); } @@ -778,16 +974,17 @@ private readonly bool CanGetTarget(IGameObject target, IGameObject subTarget) } } + internal static IBattleChara? RandomPhysicalTarget(IEnumerable tars) { - return RandomPickByJobs(tars, Job.WAR, Job.GNB, Job.MNK, Job.SAM, Job.DRG, Job.MCH, Job.DNC) + return RandomPickByJobs(tars, Job.VPR, Job.WAR, Job.GNB, Job.MNK, Job.SAM, Job.DRG, Job.MCH, Job.DNC) ?? RandomPickByJobs(tars, Job.PLD, Job.DRK, Job.NIN, Job.BRD, Job.RDM) ?? RandomObject(tars); } internal static IBattleChara? RandomMagicalTarget(IEnumerable tars) { - return RandomPickByJobs(tars, Job.SCH, Job.AST, Job.SGE, Job.BLM, Job.SMN) + return RandomPickByJobs(tars, Job.PCT, Job.SCH, Job.AST, Job.SGE, Job.BLM, Job.SMN) ?? RandomPickByJobs(tars, Job.PLD, Job.DRK, Job.NIN, Job.BRD, Job.RDM) ?? RandomObject(tars); } diff --git a/RotationSolver.Basic/Actions/BaseAction.cs b/RotationSolver.Basic/Actions/BaseAction.cs index b7d7319b2..1fc8e122e 100644 --- a/RotationSolver.Basic/Actions/BaseAction.cs +++ b/RotationSolver.Basic/Actions/BaseAction.cs @@ -132,29 +132,25 @@ public bool CanUse(out IAction act, bool isLastAbility = false, bool skipStatusP { Setting.EndSpecial = IBaseAction.ShouldEndSpecial; } + if (IBaseAction.AllEmpty) { usedUp = true; } - if (isLastAbility) - { - if (DataCenter.NextAbilityToNextGCD > ActionManagerHelper.GetCurrentAnimationLock() + DataCenter.MinAnimationLock + Service.Config.isLastAbilityTimer) return false; - } + if (isLastAbility && !IsLastAbilityUsable()) return false; if (!Info.BasicCheck(skipStatusProvideCheck, skipComboCheck, skipCastingCheck)) return false; if (!Cooldown.CooldownCheck(usedUp, gcdCountForAbility)) return false; + if (Setting.SpecialType == SpecialActionType.MeleeRange && IActionHelper.IsLastAction(IActionHelper.MovingActions)) return false; // No range actions after moving. - if (Setting.SpecialType is SpecialActionType.MeleeRange - && IActionHelper.IsLastAction(IActionHelper.MovingActions)) return false; //No range actions after moving. - - if (DataCenter.AverageTimeToKill < Config.TimeToKill) return false; - if (DataCenter.AverageTimeToKill < Config.TimeToUntargetable) return false; + if (!IsTimeToKillValid()) return false; PreviewTarget = TargetInfo.FindTarget(skipAoeCheck, skipStatusProvideCheck); if (PreviewTarget == null) return false; + if (!IBaseAction.ActionPreview) { Target = PreviewTarget.Value; @@ -163,6 +159,16 @@ public bool CanUse(out IAction act, bool isLastAbility = false, bool skipStatusP return true; } + private bool IsLastAbilityUsable() + { + return DataCenter.NextAbilityToNextGCD <= ActionManagerHelper.GetCurrentAnimationLock() + DataCenter.MinAnimationLock + Service.Config.isLastAbilityTimer; + } + + private bool IsTimeToKillValid() + { + return DataCenter.AverageTimeToKill >= Config.TimeToKill && DataCenter.AverageTimeToKill >= Config.TimeToUntargetable; + } + /// public unsafe bool Use() diff --git a/RotationSolver.Basic/Attributes/LinkDescriptionAttribute.cs b/RotationSolver.Basic/Attributes/LinkDescriptionAttribute.cs index 5fc3c97ca..83cfb7da8 100644 --- a/RotationSolver.Basic/Attributes/LinkDescriptionAttribute.cs +++ b/RotationSolver.Basic/Attributes/LinkDescriptionAttribute.cs @@ -1,24 +1,30 @@ namespace RotationSolver.Basic.Attributes; /// -/// The link to a image or web about your rotation. +/// The link to an image or web page about your rotation. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)] public class LinkDescriptionAttribute : Attribute { /// - /// The description. + /// The link description. /// - public LinkDescription LinkDescription { get; set; } + public LinkDescription LinkDescription { get; } /// - /// Constructer. + /// Constructor. /// - /// - /// + /// The URL of the link. + /// The description of the link. + /// Thrown when the URL is null or empty. public LinkDescriptionAttribute(string url, string description = "") { - LinkDescription = new() { Url = url, Description = description }; + if (string.IsNullOrEmpty(url)) + { + throw new ArgumentException("URL cannot be null or empty", nameof(url)); + } + + LinkDescription = new LinkDescription { Url = url, Description = description }; } } @@ -28,12 +34,12 @@ public LinkDescriptionAttribute(string url, string description = "") public readonly record struct LinkDescription { /// - /// Description. + /// The description of the link. /// public string Description { get; init; } /// - /// Url. + /// The URL of the link. /// public string Url { get; init; } -} +} \ No newline at end of file diff --git a/RotationSolver.Basic/Attributes/RotationDescAttribute.cs b/RotationSolver.Basic/Attributes/RotationDescAttribute.cs index 4b327ffaa..bb019c1b8 100644 --- a/RotationSolver.Basic/Attributes/RotationDescAttribute.cs +++ b/RotationSolver.Basic/Attributes/RotationDescAttribute.cs @@ -1,7 +1,7 @@ namespace RotationSolver.Basic.Attributes; /// -/// The description about the macro. If it tag at the rotation class, it means Burst. Others means the macro that this method belongs to. +/// The description about the macro. If it tags the rotation class, it means Burst. Others mean the macro that this method belongs to. /// [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class RotationDescAttribute : Attribute @@ -65,7 +65,7 @@ internal RotationDescAttribute(DescType descType) } /// - /// Constructer + /// Constructor /// /// public RotationDescAttribute(params ActionID[] actions) @@ -74,7 +74,7 @@ public RotationDescAttribute(params ActionID[] actions) } /// - /// Constructer + /// Constructor /// /// /// @@ -86,7 +86,6 @@ public RotationDescAttribute(string desc, params ActionID[] actions) private RotationDescAttribute() { - } internal static IEnumerable Merge(IEnumerable rotationDescAttributes) @@ -113,7 +112,6 @@ orderby gr.Key result.Actions = result.Actions.Union(attr.Actions); } - if (result.Type == DescType.None) return null; - return result; + return result.Type == DescType.None ? null : result; } } diff --git a/RotationSolver.Basic/Configuration/OtherConfiguration.cs b/RotationSolver.Basic/Configuration/OtherConfiguration.cs index 28e144c5a..51cca0748 100644 --- a/RotationSolver.Basic/Configuration/OtherConfiguration.cs +++ b/RotationSolver.Basic/Configuration/OtherConfiguration.cs @@ -123,10 +123,6 @@ public static Task SaveAnimationLockTime() private static string GetFilePath(string name) { var directory = Svc.PluginInterface.ConfigDirectory.FullName; -#if DEBUG - var dir = @"E:\OneDrive - stu.zafu.edu.cn\PartTime\FFXIV\RotationSolver\Resources"; - if (Directory.Exists(dir)) directory = dir; -#endif return directory + $"\\{name}.json"; } diff --git a/RotationSolver.Basic/Helpers/ActionHelper.cs b/RotationSolver.Basic/Helpers/ActionHelper.cs index c611bb157..dd40948d5 100644 --- a/RotationSolver.Basic/Helpers/ActionHelper.cs +++ b/RotationSolver.Basic/Helpers/ActionHelper.cs @@ -1,32 +1,39 @@ using Action = Lumina.Excel.GeneratedSheets.Action; - namespace RotationSolver.Basic.Helpers; internal static class ActionHelper { internal const byte GCDCooldownGroup = 58; - internal static ActionCate GetActionCate(this Action action) => (ActionCate)(action.ActionCategory.Value?.RowId ?? 0); + internal static ActionCate GetActionCate(this Action action) + { + return (ActionCate)(action.ActionCategory.Value?.RowId ?? 0); + } - internal static bool IsGeneralGCD(this Action action) => action.CooldownGroup == GCDCooldownGroup; + internal static bool IsGeneralGCD(this Action action) + { + return action.CooldownGroup == GCDCooldownGroup; + } - internal static bool IsRealGCD(this Action action) => action.IsGeneralGCD() || action.AdditionalCooldownGroup == GCDCooldownGroup; + internal static bool IsRealGCD(this Action action) + { + return action.IsGeneralGCD() || action.AdditionalCooldownGroup == GCDCooldownGroup; + } internal static byte GetCoolDownGroup(this Action action) { var group = action.IsGeneralGCD() ? action.AdditionalCooldownGroup : action.CooldownGroup; - if (group == 0) group = GCDCooldownGroup; - return group; + return group == 0 ? GCDCooldownGroup : group; } - internal static bool IsInJob(this Action i) + internal static bool IsInJob(this Action action) { - var cate = i.ClassJobCategory.Value; + var cate = action.ClassJobCategory.Value; if (cate != null) { var inJob = (bool?)cate.GetType().GetProperty(DataCenter.Job.ToString())?.GetValue(cate); - if (inJob.HasValue && !inJob.Value) return false; + return inJob.GetValueOrDefault(true); } return true; } @@ -36,10 +43,7 @@ internal static bool CanUseGCD get { var maxAhead = DataCenter.ActionAhead; - - //GCD - var canUseGCD = DataCenter.DefaultGCDRemain <= maxAhead; - return canUseGCD; + return DataCenter.DefaultGCDRemain <= maxAhead; } } -} +} \ No newline at end of file diff --git a/RotationSolver.Basic/Helpers/ActionIdHelper.cs b/RotationSolver.Basic/Helpers/ActionIdHelper.cs index 3e7b91829..9e5b8f244 100644 --- a/RotationSolver.Basic/Helpers/ActionIdHelper.cs +++ b/RotationSolver.Basic/Helpers/ActionIdHelper.cs @@ -10,20 +10,22 @@ namespace RotationSolver.Basic.Helpers; public static class ActionIdHelper { /// - /// Is this action cooling down. + /// Checks if the action is cooling down. /// - /// the action id. - /// + /// The action ID. + /// True if the action is cooling down, otherwise false. public unsafe static bool IsCoolingDown(this ActionID actionID) { - return IsCoolingDown(actionID.GetAction().GetCoolDownGroup()); + var action = actionID.GetAction(); + if (action == null) return false; + return IsCoolingDown(action.GetCoolDownGroup()); } /// - /// Is this action cooling down. + /// Checks if the action is cooling down. /// - /// - /// + /// The cooldown group. + /// True if the action is cooling down, otherwise false. public unsafe static bool IsCoolingDown(byte cdGroup) { var detail = GetCoolDownDetail(cdGroup); @@ -31,25 +33,32 @@ public unsafe static bool IsCoolingDown(byte cdGroup) } /// - /// The cd details + /// Gets the cooldown details. /// - /// - /// - public static unsafe RecastDetail* GetCoolDownDetail(byte cdGroup) => ActionManager.Instance()->GetRecastGroupDetail(cdGroup - 1); - + /// The cooldown group. + /// A pointer to the cooldown details. + public static unsafe RecastDetail* GetCoolDownDetail(byte cdGroup) + { + return ActionManager.Instance()->GetRecastGroupDetail(cdGroup - 1); + } - private static Action GetAction(this ActionID actionID) + /// + /// Gets the action associated with the action ID. + /// + /// The action ID. + /// The action associated with the action ID. + private static Action? GetAction(this ActionID actionID) { - return Svc.Data.GetExcelSheet()!.GetRow((uint)actionID)!; + return Svc.Data.GetExcelSheet()?.GetRow((uint)actionID); } /// - /// The cast time. + /// Gets the cast time of the action. /// - /// - /// + /// The action ID. + /// The cast time of the action in seconds. public unsafe static float GetCastTime(this ActionID actionID) { - return ActionManager.GetAdjustedCastTime(ActionType.Action, (uint)actionID) / 1000f; ; + return ActionManager.GetAdjustedCastTime(ActionType.Action, (uint)actionID) / 1000f; } } diff --git a/RotationSolver.Basic/Helpers/ActionManagerHelper.cs b/RotationSolver.Basic/Helpers/ActionManagerHelper.cs index 3a103b428..faad55e5d 100644 --- a/RotationSolver.Basic/Helpers/ActionManagerHelper.cs +++ b/RotationSolver.Basic/Helpers/ActionManagerHelper.cs @@ -4,10 +4,18 @@ namespace RotationSolver.Basic.Helpers { internal static class ActionManagerHelper { + private const uint DefaultActionId = 11; + private const float DefaultAnimationLock = 0.6f; + + private static unsafe ActionManager* GetActionManager() + { + return ActionManager.Instance(); + } + public static unsafe float GetCurrentAnimationLock() { - var actionManager = ActionManager.Instance(); - if (actionManager == null) return 0.6f; + var actionManager = GetActionManager(); + if (actionManager == null) return DefaultAnimationLock; var animationLockRaw = ((IntPtr)actionManager + 8); return *(float*)animationLockRaw; @@ -15,7 +23,7 @@ public static unsafe float GetCurrentAnimationLock() public static unsafe float GetRecastTime(ActionType type, uint id) { - var actionManager = ActionManager.Instance(); + var actionManager = GetActionManager(); if (actionManager == null) return 0; return actionManager->GetRecastTime(type, id); @@ -23,12 +31,12 @@ public static unsafe float GetRecastTime(ActionType type, uint id) public static unsafe float GetDefaultRecastTime() { - return GetRecastTime(ActionType.Action, 11); + return GetRecastTime(ActionType.Action, DefaultActionId); } public static unsafe float GetRecastTimeElapsed(ActionType type, uint id) { - var actionManager = ActionManager.Instance(); + var actionManager = GetActionManager(); if (actionManager == null) return 0; return actionManager->GetRecastTimeElapsed(type, id); @@ -36,7 +44,7 @@ public static unsafe float GetRecastTimeElapsed(ActionType type, uint id) public static unsafe float GetDefaultRecastTimeElapsed() { - return GetRecastTimeElapsed(ActionType.Action, 11); + return GetRecastTimeElapsed(ActionType.Action, DefaultActionId); } } } diff --git a/RotationSolver.Basic/Helpers/MarkingHelper.cs b/RotationSolver.Basic/Helpers/MarkingHelper.cs index 41e60b7a1..91cfb689d 100644 --- a/RotationSolver.Basic/Helpers/MarkingHelper.cs +++ b/RotationSolver.Basic/Helpers/MarkingHelper.cs @@ -25,12 +25,17 @@ internal enum HeadMarker : byte internal class MarkingHelper { - internal unsafe static long GetMarker(HeadMarker index) => MarkingController.Instance()->Markers[(int)index].ObjectId; + internal unsafe static long GetMarker(HeadMarker index) + { + var instance = MarkingController.Instance(); + if (instance == null) return 0; + return instance->Markers[(int)index].ObjectId; + } internal static bool HaveAttackChara => AttackSignTargets.Any(id => id != 0); - internal static long[] AttackSignTargets => - [ + internal static long[] AttackSignTargets => new long[] + { GetMarker(HeadMarker.Attack1), GetMarker(HeadMarker.Attack2), GetMarker(HeadMarker.Attack3), @@ -39,15 +44,15 @@ internal class MarkingHelper GetMarker(HeadMarker.Attack6), GetMarker(HeadMarker.Attack7), GetMarker(HeadMarker.Attack8), - ]; + }; - internal static long[] StopTargets => - [ + internal static long[] StopTargets => new long[] + { GetMarker(HeadMarker.Stop1), GetMarker(HeadMarker.Stop2), - ]; + }; - internal unsafe static IEnumerable FilterStopCharaes(IEnumerable charas) + internal unsafe static IEnumerable FilterStopCharacters(IEnumerable charas) { var ids = StopTargets.Where(id => id != 0); return charas.Where(b => !ids.Contains((long)b.GameObjectId)); diff --git a/RotationSolver.Basic/Helpers/ObjectHelper.cs b/RotationSolver.Basic/Helpers/ObjectHelper.cs index 0695bca30..dc533a736 100644 --- a/RotationSolver.Basic/Helpers/ObjectHelper.cs +++ b/RotationSolver.Basic/Helpers/ObjectHelper.cs @@ -6,6 +6,7 @@ using ECommons.GameHelpers; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Event; +using FFXIVClientStructs.FFXIV.Client.Graphics; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Common.Component.BGCollision; using Lumina.Excel.GeneratedSheets; @@ -201,40 +202,41 @@ internal static bool IsAlive(this IGameObject obj) /// public static unsafe ObjectKind GetObjectKind(this IGameObject obj) => (ObjectKind)obj.Struct()->ObjectKind; + /// + /// Determines whether the specified game object is a top priority hostile target. + /// + /// The game object to check. + /// + /// true if the game object is a top priority hostile target; otherwise, false. + /// internal static bool IsTopPriorityHostile(this IGameObject obj) { + if (obj == null) return false; + var fateId = DataCenter.FateId; if (obj is IBattleChara b && b.StatusList != null && b.StatusList.Any(StatusHelper.IsPriority)) return true; if (Service.Config.ChooseAttackMark && MarkingHelper.AttackSignTargets.FirstOrDefault(id => id != 0) == (long)obj.GameObjectId) return true; - //Fate + // Fate if (Service.Config.TargetFatePriority && fateId != 0 && obj.FateId() == fateId) return true; var icon = obj.GetNamePlateIcon(); - //Hunting log and weapon. - if (Service.Config.TargetHuntingRelicLevePriority && icon - is 60092 //Hunting - or 60096 //Weapon - or 71244 //Leve - ) return true; + // Hunting log and weapon + if (Service.Config.TargetHuntingRelicLevePriority && icon is 60092 or 60096 or 71244) return true; - if (Service.Config.TargetQuestPriority && (icon - is 71204 //Main Quest - or 71144 //Major Quest - or 71224 //Other Quest - or 71344 //Major Quest - || obj.GetEventType() is EventHandlerType.Quest)) return true; + // Quest + if (Service.Config.TargetQuestPriority && (icon is 71204 or 71144 or 71224 or 71344 || obj.GetEventType() is EventHandlerType.Quest)) return true; - var npc = obj as IBattleChara; - if (npc != null && DataCenter.PrioritizedNameIds.Contains(npc.NameId)) return true; + if (obj is IBattleChara npc && DataCenter.PrioritizedNameIds.Contains(npc.NameId)) return true; return false; } internal static unsafe uint GetNamePlateIcon(this IGameObject obj) => obj.Struct()->NamePlateIconId; + internal static unsafe EventHandlerType GetEventType(this IGameObject obj) => obj.Struct()->EventId.ContentId; internal static unsafe BattleNpcSubKind GetBattleNPCSubKind(this IGameObject obj) => (BattleNpcSubKind)obj.Struct()->SubKind; @@ -242,12 +244,19 @@ or 71344 //Major Quest internal static unsafe uint FateId(this IGameObject obj) => obj.Struct()->FateId; static readonly Dictionary _effectRangeCheck = []; + + /// + /// Determines whether the specified game object can be interrupted. + /// + /// The game object to check. + /// + /// true if the game object can be interrupted; otherwise, false. + /// internal static bool CanInterrupt(this IGameObject o) { if (o is not IBattleChara b) return false; var baseCheck = b.IsCasting && b.IsCastInterruptible && b.TotalCastTime >= 2; - if (!baseCheck) return false; if (!Service.Config.InterruptibleMoreCheck) return false; @@ -256,8 +265,8 @@ internal static bool CanInterrupt(this IGameObject o) var act = Service.GetSheet().GetRow(b.CastActionId); if (act == null) return _effectRangeCheck[id] = false; - if (act.CastType is 3 or 4) return _effectRangeCheck[id] = false; - if (act.EffectRange is > 0 and < 8) return _effectRangeCheck[id] = false; + if (act.CastType is 3 or 4 || (act.EffectRange is > 0 and < 8)) return _effectRangeCheck[id] = false; + return _effectRangeCheck[id] = true; } @@ -308,6 +317,13 @@ public static bool IsDying(this IBattleChara b) return b.GetTimeToKill() <= Service.Config.DyingTimeToKill || b.GetHealthRatio() < Service.Config.IsDyingConfig; } + /// + /// Determines whether the specified battle character is currently in combat. + /// + /// The battle character to check. + /// + /// true if the battle character is in combat; otherwise, false. + /// internal static unsafe bool InCombat(this IBattleChara obj) { if (obj == null || obj.Struct() == null) return false; @@ -316,6 +332,14 @@ internal static unsafe bool InCombat(this IBattleChara obj) private static readonly TimeSpan CheckSpan = TimeSpan.FromSeconds(2.5); + /// + /// Calculates the estimated time to kill the specified battle character. + /// + /// The battle character to calculate the time to kill for. + /// If set to true, calculates the total time to kill; otherwise, calculates the remaining time to kill. + /// + /// The estimated time to kill the battle character in seconds, or if the calculation cannot be performed. + /// internal static float GetTimeToKill(this IBattleChara b, bool wholeTime = false) { if (b == null) return float.NaN; @@ -347,6 +371,13 @@ internal static float GetTimeToKill(this IBattleChara b, bool wholeTime = false) return (float)timespan.TotalSeconds / ratioReduce * (wholeTime ? 1 : ratioNow); } + /// + /// Determines if the specified battle character has been attacked within the last second. + /// + /// The battle character to check. + /// + /// true if the battle character has been attacked within the last second; otherwise, false. + /// internal static bool IsAttacked(this IBattleChara b) { if (b == null) return false; @@ -360,15 +391,32 @@ internal static bool IsAttacked(this IBattleChara b) return false; } + /// + /// Determines if the player can see the specified game object. + /// + /// The game object to check visibility for. + /// + /// true if the player can see the specified game object; otherwise, false. + /// internal static unsafe bool CanSee(this IGameObject b) { - var point = Player.Object.Position + Vector3.UnitY * Player.GameObject->Height; + var player = Player.Object; + if (player == null || b == null) return false; + + const uint specificEnemyId = 3830; // Bioculture Node in Aetherial Chemical Research Facility + if (b.GameObjectId == specificEnemyId) + { + return true; + } + + var point = player.Position + Vector3.UnitY * Player.GameObject->Height; var tarPt = b.Position + Vector3.UnitY * b.Struct()->Height; var direction = tarPt - point; int* unknown = stackalloc int[] { 0x4000, 0, 0x4000, 0 }; - RaycastHit hit = default; + RaycastHit hit; + var ray = new Ray(point, direction); return !FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->BGCollisionModule ->RaycastMaterialFilter(&hit, &point, &direction, direction.Length(), 1, unknown); @@ -386,30 +434,63 @@ public static float GetHealthRatio(this IGameObject g) return (float)b.CurrentHp / b.MaxHp; } + /// + /// Determines the positional relationship of the player relative to the enemy. + /// + /// The enemy game object. + /// + /// An value indicating whether the player is in front, at the rear, or on the flank of the enemy. + /// internal static EnemyPositional FindEnemyPositional(this IGameObject enemy) { + if (enemy == null || Player.Object == null) return EnemyPositional.None; + Vector3 pPosition = enemy.Position; Vector2 faceVec = enemy.GetFaceVector(); Vector3 dir = Player.Object.Position - pPosition; - Vector2 dirVec = new(dir.Z, dir.X); + Vector2 dirVec = new Vector2(dir.Z, dir.X); double angle = faceVec.AngleTo(dirVec); - if (angle < Math.PI / 4) return EnemyPositional.Front; - else if (angle > Math.PI * 3 / 4) return EnemyPositional.Rear; + const double frontAngle = Math.PI / 4; + const double rearAngle = Math.PI * 3 / 4; + + if (angle < frontAngle) return EnemyPositional.Front; + else if (angle > rearAngle) return EnemyPositional.Rear; return EnemyPositional.Flank; } + /// + /// Gets the facing direction vector of the game object. + /// + /// The game object. + /// + /// A representing the facing direction of the game object. + /// internal static Vector2 GetFaceVector(this IGameObject obj) { + if (obj == null) return Vector2.Zero; + float rotation = obj.Rotation; - return new((float)Math.Cos(rotation), (float)Math.Sin(rotation)); + return new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)); } + /// + /// Calculates the angle between two vectors. + /// + /// The first vector. + /// The second vector. + /// + /// The angle in radians between the two vectors. + /// internal static double AngleTo(this Vector2 vec1, Vector2 vec2) { - return Math.Acos(Vector2.Dot(vec1, vec2) / vec1.Length() / vec2.Length()); + double lengthProduct = vec1.Length() * vec2.Length(); + if (lengthProduct == 0) return 0; + + double dotProduct = Vector2.Dot(vec1, vec2); + return Math.Acos(dotProduct / lengthProduct); } /// @@ -419,12 +500,10 @@ internal static double AngleTo(this Vector2 vec1, Vector2 vec2) /// public static float DistanceToPlayer(this IGameObject? obj) { - if (obj == null) return float.MaxValue; - var player = Player.Object; - if (player == null) return float.MaxValue; + if (obj == null || Player.Object == null) return float.MaxValue; - var distance = Vector3.Distance(player.Position, obj.Position) - player.HitboxRadius; - distance -= obj.HitboxRadius; + var player = Player.Object; + var distance = Vector3.Distance(player.Position, obj.Position) - (player.HitboxRadius + obj.HitboxRadius); return distance; } } diff --git a/RotationSolver.Basic/Helpers/ReflectionHelper.cs b/RotationSolver.Basic/Helpers/ReflectionHelper.cs index ae096df03..4fc4ac27e 100644 --- a/RotationSolver.Basic/Helpers/ReflectionHelper.cs +++ b/RotationSolver.Basic/Helpers/ReflectionHelper.cs @@ -6,14 +6,14 @@ internal static PropertyInfo[] GetStaticProperties(this Type? type) { if (type == null) return Array.Empty(); - var props = from prop in type.GetRuntimeProperties() - where typeof(T).IsAssignableFrom(prop.PropertyType) - && prop.GetMethod is MethodInfo info - && info.IsPublic && info.IsStatic - && info.GetCustomAttribute() == null - select prop; - - return props.Union(type.BaseType?.GetStaticProperties() ?? Array.Empty()).ToArray(); + var properties = from prop in type.GetRuntimeProperties() + where typeof(T).IsAssignableFrom(prop.PropertyType) + && prop.GetMethod is MethodInfo methodInfo + && methodInfo.IsPublic && methodInfo.IsStatic + && prop.GetCustomAttribute() == null + select prop; + + return properties.Union(type.BaseType?.GetStaticProperties() ?? Array.Empty()).ToArray(); } internal static IEnumerable GetAllMethodInfo(this Type? type) @@ -29,10 +29,13 @@ internal static IEnumerable GetAllMethodInfo(this Type? type) internal static PropertyInfo? GetPropertyInfo(this Type type, string name) { - foreach (var item in type.GetRuntimeProperties()) + foreach (var property in type.GetRuntimeProperties()) { - if (item.Name == name && item.GetMethod is MethodInfo info - && info.IsStatic) return item; + if (property.Name == name && property.GetMethod is MethodInfo methodInfo + && methodInfo.IsStatic) + { + return property; + } } return type.BaseType?.GetPropertyInfo(name); @@ -42,10 +45,13 @@ internal static IEnumerable GetAllMethodInfo(this Type? type) { if (type == null) return null; - foreach (var item in type.GetRuntimeMethods()) + foreach (var method in type.GetRuntimeMethods()) { - if (item.Name == name && item.IsStatic - && !item.IsConstructor && item.ReturnType == typeof(bool)) return item; + if (method.Name == name && method.IsStatic + && !method.IsConstructor && method.ReturnType == typeof(bool)) + { + return method; + } } return type.BaseType?.GetMethodInfo(name); diff --git a/RotationSolver.Basic/Helpers/TargetFilter.cs b/RotationSolver.Basic/Helpers/TargetFilter.cs index 416432449..840721f12 100644 --- a/RotationSolver.Basic/Helpers/TargetFilter.cs +++ b/RotationSolver.Basic/Helpers/TargetFilter.cs @@ -11,45 +11,44 @@ public static class TargetFilter { #region Find one target /// - /// Get the deadth ones in the list. + /// Get the dead ones in the list. /// - /// - /// - public unsafe static IEnumerable GetDeath(this IEnumerable charas) => charas.Where(item => - { - if (item == null) return false; - if (!item.IsDead) return false; - if (item.CurrentHp != 0) return false; - - if (!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; - }); + /// The list of characters. + /// The dead characters. + public static IEnumerable GetDeath(this IEnumerable charas) => charas.Where(item => + { + 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; + }); /// /// Get the specific roles members. /// - /// - /// - /// + /// The list of objects. + /// The roles to filter by. + /// The objects that match the roles. public static IEnumerable GetJobCategory(this IEnumerable objects, params JobRole[] roles) - => roles.SelectMany(role => objects.Where(obj => obj.IsJobCategory(role))); + { + var validJobs = roles.SelectMany(role => Service.GetSheet() + .Where(job => role == job.GetJobRole()) + .Select(job => (byte)job.RowId)) + .ToHashSet(); + + return objects.Where(obj => obj.IsJobs(validJobs)); + } /// /// Is the target the role. /// - /// - /// - /// + /// The game object. + /// The role to check. + /// True if the object is of the specified role, otherwise false. public static bool IsJobCategory(this IGameObject obj, JobRole role) { - SortedSet validJobs = new(Service.GetSheet() + var validJobs = new HashSet(Service.GetSheet() .Where(job => role == job.GetJobRole()) .Select(job => (byte)job.RowId)); @@ -59,15 +58,15 @@ public static bool IsJobCategory(this IGameObject obj, JobRole role) /// /// Is the target in the jobs. /// - /// - /// - /// + /// The game object. + /// The valid jobs. + /// True if the object is in the valid jobs, otherwise false. public static bool IsJobs(this IGameObject obj, params Job[] validJobs) { - return obj.IsJobs(new SortedSet(validJobs.Select(j => (byte)(uint)j))); + return obj.IsJobs(new HashSet(validJobs.Select(j => (byte)(uint)j))); } - private static bool IsJobs(this IGameObject obj, SortedSet validJobs) + private static bool IsJobs(this IGameObject obj, HashSet validJobs) { if (obj is not IBattleChara b) return false; return validJobs.Contains((byte?)b.ClassJob.GameData?.RowId ?? 0); @@ -77,10 +76,10 @@ private static bool IsJobs(this IGameObject obj, SortedSet validJobs) /// /// Get the in . /// - /// - /// - /// - /// + /// The type of objects. + /// The list of objects. + /// The radius to filter by. + /// The objects within the radius. public static IEnumerable GetObjectInRadius(this IEnumerable objects, float radius) where T : IGameObject => objects.Where(o => o.DistanceToPlayer() <= radius); } diff --git a/RotationSolver.Basic/Rotations/Basic/DarkKnightRotation.cs b/RotationSolver.Basic/Rotations/Basic/DarkKnightRotation.cs index 979dc1cb3..5c5e9f6a0 100644 --- a/RotationSolver.Basic/Rotations/Basic/DarkKnightRotation.cs +++ b/RotationSolver.Basic/Rotations/Basic/DarkKnightRotation.cs @@ -99,6 +99,18 @@ public static byte LowDeliriumStacks return stacks == byte.MaxValue ? (byte)3 : stacks; } } + + /// + public override void DisplayStatus() + { + ImGui.Text("BloodWeaponStacks: " + BloodWeaponStacks.ToString()); + ImGui.Text("DeliriumStacks: " + DeliriumStacks.ToString()); + ImGui.Text("LowDeliriumStacks: " + LowDeliriumStacks.ToString()); + ImGui.Text("ShadowTime: " + ShadowTime.ToString()); + ImGui.Text("DarkSideTime: " + DarkSideTime.ToString()); + ImGui.Text("HasDarkArts: " + HasDarkArts.ToString()); + ImGui.Text("Blood: " + Blood.ToString()); + } #endregion static partial void ModifyHardSlashPvE(ref ActionSetting setting) @@ -365,12 +377,4 @@ static partial void ModifyPlungePvP(ref ActionSetting setting) { setting.SpecialType = SpecialActionType.MovingForward; } - - /// - public override void DisplayStatus() - { - ImGui.Text("BloodWeaponStacks: " + BloodWeaponStacks.ToString()); - ImGui.Text("DeliriumStacks: " + DeliriumStacks.ToString()); - ImGui.Text("LowDeliriumStacks: " + LowDeliriumStacks.ToString()); - } } diff --git a/RotationSolver.Basic/Rotations/Basic/WarriorRotation.cs b/RotationSolver.Basic/Rotations/Basic/WarriorRotation.cs index 25de7b17b..df867e0ff 100644 --- a/RotationSolver.Basic/Rotations/Basic/WarriorRotation.cs +++ b/RotationSolver.Basic/Rotations/Basic/WarriorRotation.cs @@ -40,6 +40,7 @@ public override void DisplayStatus() { ImGui.Text("InnerReleaseStacks: " + InnerReleaseStacks.ToString()); ImGui.Text("BerserkStacks: " + BerserkStacks.ToString()); + ImGui.Text("BeastGaugeValue: " + BeastGauge.ToString()); } private sealed protected override IBaseAction TankStance => DefiancePvE; diff --git a/RotationSolver.Basic/Rotations/Basic/WhiteMageRotation.cs b/RotationSolver.Basic/Rotations/Basic/WhiteMageRotation.cs index 2acbb1d87..13f26512a 100644 --- a/RotationSolver.Basic/Rotations/Basic/WhiteMageRotation.cs +++ b/RotationSolver.Basic/Rotations/Basic/WhiteMageRotation.cs @@ -9,40 +9,43 @@ partial class WhiteMageRotation #region Job Gauge /// - /// + /// Represents the number of Lily stacks. /// public static byte Lily => JobGauge.Lily; /// - /// + /// Represents the number of Blood Lily stacks. /// public static byte BloodLily => JobGauge.BloodLily; + /// + /// Gets the raw Lily timer value in seconds. + /// static float LilyTimeRaw => JobGauge.LilyTimer / 1000f; /// - /// + /// Gets the Lily timer value adjusted by the default GCD remain. /// public static float LilyTime => LilyTimeRaw + DataCenter.DefaultGCDRemain; /// - /// + /// Determines if the Lily timer will expire after the specified time. /// - /// - /// + /// The time in seconds to check against the Lily timer. + /// True if the Lily timer will expire after the specified time; otherwise, false. protected static bool LilyAfter(float time) => LilyTime <= time; /// - /// + /// Determines if the Lily timer will expire after a specified number of GCDs and an optional offset. /// - /// - /// - /// + /// The number of GCDs to check against the Lily timer. + /// An optional offset in seconds to add to the GCD time. + /// True if the Lily timer will expire after the specified number of GCDs and offset; otherwise, false. protected static bool LilyAfterGCD(uint gcdCount = 0, float offset = 0) => LilyAfter(GCDTime(gcdCount, offset)); /// - /// Holds the remaining amount of SacredSight stacks + /// Gets the remaining number of Sacred Sight stacks. /// public static byte SacredSightStacks { @@ -57,6 +60,8 @@ public static byte SacredSightStacks public override void DisplayStatus() { ImGui.Text("SacredSightStacks: " + SacredSightStacks.ToString()); + ImGui.Text("LilyTime: " + LilyTime.ToString()); + ImGui.Text("BloodLilyStacks: " + BloodLily.ToString()); } #endregion diff --git a/RotationSolver/Commands/RSCommands_Actions.cs b/RotationSolver/Commands/RSCommands_Actions.cs index d43ef685c..27ea37df2 100644 --- a/RotationSolver/Commands/RSCommands_Actions.cs +++ b/RotationSolver/Commands/RSCommands_Actions.cs @@ -96,7 +96,12 @@ public static void DoAction() if (tar != null && tar != Player.Object && tar.IsEnemy()) { DataCenter.HostileTarget = tar; - if (!DataCenter.IsManual) Svc.Targets.Target = tar; + if (!DataCenter.IsManual + && (Service.Config.SwitchTargetFriendly || ((Svc.Targets.Target?.IsEnemy() ?? true) + || Svc.Targets.Target?.GetObjectKind() == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Treasure))) + { + Svc.Targets.Target = tar; + } } }