diff --git a/RotationSolver.Basic/Actions/ActionTargetInfo.cs b/RotationSolver.Basic/Actions/ActionTargetInfo.cs index 6a4c63c32..89f74ca7a 100644 --- a/RotationSolver.Basic/Actions/ActionTargetInfo.cs +++ b/RotationSolver.Basic/Actions/ActionTargetInfo.cs @@ -6,6 +6,7 @@ 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; @@ -39,24 +40,31 @@ public struct ActionTargetInfo(IBaseAction action) /// Is this action friendly. /// public readonly bool IsTargetFriendly => action.Setting.IsFriendly; + #region Target Stuff - #region Target Finder. private readonly IEnumerable GetCanTargets(bool skipStatusProvideCheck, TargetType type) { - var items = TargetFilter.GetObjectInRadius(DataCenter.AllTargets, Range); - var objs = new List(items.Count()); + var allTargets = DataCenter.AllTargets; + if (allTargets == null) return Enumerable.Empty(); - foreach (var obj in items) - { - if (type == TargetType.Heal && obj.GetHealthRatio() == 1) continue; + var filteredTargets = TargetFilter.GetObjectInRadius(allTargets, Range); + var validTargets = new List(filteredTargets.Count()); - if (!GeneralCheck(obj, skipStatusProvideCheck)) continue; - objs.Add(obj); + foreach (var target in filteredTargets) + { + if (type == TargetType.Heal && target.GetHealthRatio() == 1) continue; + if (!GeneralCheck(target, skipStatusProvideCheck)) continue; + validTargets.Add(target); } var isAuto = !DataCenter.IsManual || IsTargetFriendly; - return objs.Where(b => isAuto || b.GameObjectId == Svc.Targets.Target?.GameObjectId || b.GameObjectId == Player.Object.GameObjectId) - .Where(InViewTarget).Where(CanUseTo).Where(action.Setting.CanTarget); + var playerObjectId = Player.Object?.GameObjectId; + var targetObjectId = Svc.Targets.Target?.GameObjectId; + + return validTargets.Where(b => isAuto || b.GameObjectId == targetObjectId || b.GameObjectId == playerObjectId) + .Where(InViewTarget) + .Where(CanUseTo) + .Where(action.Setting.CanTarget); } private readonly List GetCanAffects(bool skipStatusProvideCheck, TargetType type) @@ -198,8 +206,6 @@ private readonly bool CheckResistance(IGameObject IGameObject) return false; } } - - return true; } @@ -690,16 +696,27 @@ private readonly bool CanGetTarget(IGameObject target, IGameObject subTarget) IBattleChara? FindHostileRaw() { - IGameObjects = DataCenter.TargetingType switch + if (DataCenter.IsPvP) { - TargetingType.Small => IGameObjects.OrderBy(p => p.HitboxRadius), - TargetingType.HighHP => IGameObjects.OrderByDescending(p => p is IBattleChara b ? b.CurrentHp : 0), - TargetingType.LowHP => IGameObjects.OrderBy(p => p is IBattleChara b ? b.CurrentHp : 0), - TargetingType.HighMaxHP => IGameObjects.OrderByDescending(p => p is IBattleChara b ? b.MaxHp : 0), - TargetingType.LowMaxHP => IGameObjects.OrderBy(p => p is IBattleChara b ? b.MaxHp : 0), - _ => IGameObjects.OrderByDescending(p => p.HitboxRadius), - }; - return IGameObjects.FirstOrDefault(); + return IGameObjects + .OfType() + .OrderBy(p => p.CurrentHp) + .FirstOrDefault(); + } + else + { + var orderedGameObjects = DataCenter.TargetingType switch + { + TargetingType.Small => IGameObjects.OrderBy(p => p.HitboxRadius), + TargetingType.HighHP => IGameObjects.OrderByDescending(p => p is IBattleChara b ? b.CurrentHp : 0), + TargetingType.LowHP => IGameObjects.OrderBy(p => p is IBattleChara b ? b.CurrentHp : 0), + TargetingType.HighMaxHP => IGameObjects.OrderByDescending(p => p is IBattleChara b ? b.MaxHp : 0), + TargetingType.LowMaxHP => IGameObjects.OrderBy(p => p is IBattleChara b ? b.MaxHp : 0), + _ => IGameObjects.OrderByDescending(p => p.HitboxRadius), + }; + + return orderedGameObjects.OfType().FirstOrDefault(); + } } IBattleChara? FindBeAttackedTarget() diff --git a/RotationSolver.Basic/Configuration/Configs.cs b/RotationSolver.Basic/Configuration/Configs.cs index c07c9f1d7..509d40858 100644 --- a/RotationSolver.Basic/Configuration/Configs.cs +++ b/RotationSolver.Basic/Configuration/Configs.cs @@ -135,6 +135,10 @@ public const string Filter = TargetConfig)] private static readonly bool _filterStopMark = true; + [ConditionBool, UI("Treat 1hp targets as invincible.", + Filter = TargetConfig)] + private static readonly bool _filterOneHPInvincible = true; + [ConditionBool, UI("Teaching mode", Filter = UiInformation)] private static readonly bool _teachingMode = false; diff --git a/RotationSolver.Basic/DataCenter.cs b/RotationSolver.Basic/DataCenter.cs index 968c515c1..66ff5b9e0 100644 --- a/RotationSolver.Basic/DataCenter.cs +++ b/RotationSolver.Basic/DataCenter.cs @@ -295,14 +295,17 @@ public static IBattleChara[] AllHostileTargets get { return AllTargets.Where(b => - { + { + //Not enemy. if (!b.IsEnemy()) return false; //Dead. if (b.CurrentHp <= 1) return false; + //Not targetable. if (!b.IsTargetable) return false; + //Invincible. if (b.StatusList.Any(StatusHelper.IsInvincible)) return false; return true; }).ToArray(); @@ -318,20 +321,22 @@ public static IBattleChara? DeathTarget { get { + // Ensure AllianceMembers and PartyMembers are not null + if (AllianceMembers == null || PartyMembers == null) return null; + var deathAll = AllianceMembers.GetDeath(); var deathParty = PartyMembers.GetDeath(); if (deathParty.Any()) { - var deathT = deathParty.GetJobCategory(JobRole.Tank); + var deathT = deathParty.GetJobCategory(JobRole.Tank).ToList(); + var deathH = deathParty.GetJobCategory(JobRole.Healer).ToList(); - if (deathT.Count() > 1) + if (deathT.Count > 1) { return deathT.FirstOrDefault(); } - var deathH = deathParty.GetJobCategory(JobRole.Healer); - if (deathH.Any()) return deathH.FirstOrDefault(); if (deathT.Any()) return deathT.FirstOrDefault(); @@ -341,10 +346,11 @@ public static IBattleChara? DeathTarget if (deathAll.Any() && Service.Config.RaiseAll) { - var deathAllH = deathAll.GetJobCategory(JobRole.Healer); + var deathAllH = deathAll.GetJobCategory(JobRole.Healer).ToList(); + var deathAllT = deathAll.GetJobCategory(JobRole.Tank).ToList(); + if (deathAllH.Any()) return deathAllH.FirstOrDefault(); - var deathAllT = deathAll.GetJobCategory(JobRole.Tank); if (deathAllT.Any()) return deathAllT.FirstOrDefault(); return deathAll.FirstOrDefault(); diff --git a/RotationSolver.Basic/Helpers/ObjectHelper.cs b/RotationSolver.Basic/Helpers/ObjectHelper.cs index c566d6f67..0695bca30 100644 --- a/RotationSolver.Basic/Helpers/ObjectHelper.cs +++ b/RotationSolver.Basic/Helpers/ObjectHelper.cs @@ -10,8 +10,6 @@ using FFXIVClientStructs.FFXIV.Common.Component.BGCollision; using Lumina.Excel.GeneratedSheets; using RotationSolver.Basic.Configuration; -using System.Security.Cryptography; -using System.Text; using System.Text.RegularExpressions; namespace RotationSolver.Basic.Helpers; @@ -22,10 +20,10 @@ namespace RotationSolver.Basic.Helpers; public static class ObjectHelper { static readonly EventHandlerType[] _eventType = - [ + { EventHandlerType.TreasureHuntDirector, EventHandlerType.Quest, - ]; + }; internal static BNpcBase? GetObjectNPC(this IGameObject obj) { @@ -35,8 +33,10 @@ public static class ObjectHelper internal static bool CanProvoke(this IGameObject target) { + if (target == null) return false; + //Removed the listed names. - IEnumerable names = []; + IEnumerable names = Array.Empty(); if (OtherConfiguration.NoProvokeNames.TryGetValue(Svc.ClientState.TerritoryType, out var ns1)) names = names.Union(ns1); @@ -73,79 +73,58 @@ internal static unsafe bool IsOthersPlayers(this IGameObject obj) return false; } - internal static bool IsAttackable(this IBattleChara IBattleChara) + internal static bool IsAttackable(this IBattleChara battleChara) { //Dead. - if (IBattleChara.CurrentHp <= 1) return false; + if (Service.Config.FilterOneHpInvincible && battleChara.CurrentHp <= 1) return false; - if (IBattleChara.StatusList.Any(StatusHelper.IsInvincible)) return false; + if (battleChara.StatusList.Any(StatusHelper.IsInvincible)) return false; if (Svc.ClientState == null) return false; //In No Hostiles Names - IEnumerable names = []; + IEnumerable names = Array.Empty(); if (OtherConfiguration.NoHostileNames.TryGetValue(Svc.ClientState.TerritoryType, out var ns1)) names = names.Union(ns1); - if (names.Any(n => !string.IsNullOrEmpty(n) && new Regex(n).Match(IBattleChara.Name.TextValue).Success)) return false; + if (names.Any(n => !string.IsNullOrEmpty(n) && new Regex(n).Match(battleChara.Name.TextValue).Success)) return false; //Fate if (DataCenter.TerritoryContentType != TerritoryContentType.Eureka) { - var tarFateId = IBattleChara.FateId(); + var tarFateId = battleChara.FateId(); if (tarFateId != 0 && tarFateId != DataCenter.FateId) return false; } if (Service.Config.AddEnemyListToHostile) { - if (IBattleChara.IsInEnemiesList()) return true; + if (battleChara.IsInEnemiesList()) return true; //Only attack if (Service.Config.OnlyAttackInEnemyList) return false; } //Tar on me - if (IBattleChara.TargetObject == Player.Object - || IBattleChara.TargetObject?.OwnerId == Player.Object.GameObjectId) return true; + if (battleChara.TargetObject == Player.Object + || battleChara.TargetObject?.OwnerId == Player.Object.GameObjectId) return true; //Remove other's treasure. - if (IBattleChara.IsOthersPlayers()) return false; + if (battleChara.IsOthersPlayers()) return false; - if (IBattleChara.IsTopPriorityHostile()) return true; + if (battleChara.IsTopPriorityHostile()) return true; if (Service.CountDownTime > 0 || DataCenter.IsPvP) return true; return DataCenter.RightNowTargetToHostileType switch { TargetHostileType.AllTargetsCanAttack => true, - TargetHostileType.TargetsHaveTarget => IBattleChara.TargetObject is IBattleChara, - TargetHostileType.AllTargetsWhenSolo => DataCenter.PartyMembers.Length < 2 || IBattleChara.TargetObject is IBattleChara, + TargetHostileType.TargetsHaveTarget => battleChara.TargetObject is IBattleChara, + TargetHostileType.AllTargetsWhenSolo => DataCenter.PartyMembers.Length < 2 || battleChara.TargetObject is IBattleChara, TargetHostileType.AllTargetsWhenSoloInDuty => (DataCenter.PartyMembers.Length < 2 && Svc.Condition[ConditionFlag.BoundByDuty]) - || IBattleChara.TargetObject is IBattleChara, + || battleChara.TargetObject is IBattleChara, _ => true, }; } - - internal static string EncryptString(this IPlayerCharacter player) - { - if (player == null) return string.Empty; - - try - { - byte[] inputByteArray = Encoding.UTF8.GetBytes(player.HomeWorld.GameData!.InternalName.ToString() - + " - " + player.Name.ToString() + "U6Wy.zCG"); - - var tmpHash = MD5.HashData(inputByteArray); - var retB = Convert.ToBase64String(tmpHash); - return retB; - } - catch (Exception ex) - { - Svc.Log.Warning(ex, "Failed to read the player's name and world."); - return string.Empty; - } - } - internal static unsafe bool IsInEnemiesList(this IBattleChara IBattleChara) { var addons = Service.GetAddons(); @@ -155,10 +134,14 @@ internal static unsafe bool IsInEnemiesList(this IBattleChara IBattleChara) var enemy = (AddonEnemyList*)addon; var numArray = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->GetUIModule()->GetRaptureAtkModule()->AtkModule.AtkArrayDataHolder.NumberArrays[19]; - List list = new(enemy->EnemyCount); + if (numArray == null) return false; + + const int baseIndex = 8; + const int step = 6; + for (var i = 0; i < enemy->EnemyCount; i++) { - var id = (uint)numArray->IntArray[8 + i * 6]; + var id = (uint)numArray->IntArray[baseIndex + i * step]; if (IBattleChara.GameObjectId == id) return true; } @@ -221,6 +204,11 @@ internal static bool IsAlive(this IGameObject obj) internal static bool IsTopPriorityHostile(this IGameObject obj) { 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 if (Service.Config.TargetFatePriority && fateId != 0 && obj.FateId() == fateId) return true; @@ -240,11 +228,6 @@ or 71224 //Other Quest or 71344 //Major Quest || obj.GetEventType() is EventHandlerType.Quest)) return true; - 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; - - var npc = obj as IBattleChara; if (npc != null && DataCenter.PrioritizedNameIds.Contains(npc.NameId)) return true; @@ -327,6 +310,7 @@ public static bool IsDying(this IBattleChara b) internal static unsafe bool InCombat(this IBattleChara obj) { + if (obj == null || obj.Struct() == null) return false; return obj.Struct()->Character.InCombat; } @@ -355,6 +339,7 @@ internal static float GetTimeToKill(this IBattleChara b, bool wholeTime = false) if (startTime == DateTime.MinValue || timespan < CheckSpan) return float.NaN; var ratioNow = b.GetHealthRatio(); + if (float.IsNaN(ratioNow)) return float.NaN; var ratioReduce = thatTimeRatio - ratioNow; if (ratioReduce <= 0) return float.NaN; @@ -364,6 +349,7 @@ internal static float GetTimeToKill(this IBattleChara b, bool wholeTime = false) internal static bool IsAttacked(this IBattleChara b) { + if (b == null) return false; foreach (var (id, time) in DataCenter.AttackedTargets) { if (id == b.GameObjectId) diff --git a/RotationSolver.Basic/Helpers/ReflectionHelper.cs b/RotationSolver.Basic/Helpers/ReflectionHelper.cs index 737957a8a..ae096df03 100644 --- a/RotationSolver.Basic/Helpers/ReflectionHelper.cs +++ b/RotationSolver.Basic/Helpers/ReflectionHelper.cs @@ -4,7 +4,7 @@ internal static class ReflectionHelper { internal static PropertyInfo[] GetStaticProperties(this Type? type) { - if (type == null) return []; + if (type == null) return Array.Empty(); var props = from prop in type.GetRuntimeProperties() where typeof(T).IsAssignableFrom(prop.PropertyType) @@ -13,24 +13,22 @@ where typeof(T).IsAssignableFrom(prop.PropertyType) && info.GetCustomAttribute() == null select prop; - return props.Union(type.BaseType.GetStaticProperties()).ToArray(); + return props.Union(type.BaseType?.GetStaticProperties() ?? Array.Empty()).ToArray(); } internal static IEnumerable GetAllMethodInfo(this Type? type) { - if (type == null) return []; + if (type == null) return Enumerable.Empty(); var methods = from method in type.GetRuntimeMethods() where !method.IsConstructor select method; - return methods.Union(type.BaseType.GetAllMethodInfo()); + return methods.Union(type.BaseType?.GetAllMethodInfo() ?? Enumerable.Empty()); } internal static PropertyInfo? GetPropertyInfo(this Type type, string name) { - if (type == null) return null; - foreach (var item in type.GetRuntimeProperties()) { if (item.Name == name && item.GetMethod is MethodInfo info @@ -50,6 +48,6 @@ internal static IEnumerable GetAllMethodInfo(this Type? type) && !item.IsConstructor && item.ReturnType == typeof(bool)) return item; } - return type.BaseType.GetMethodInfo(name); + return type.BaseType?.GetMethodInfo(name); } } diff --git a/RotationSolver.Basic/Helpers/StatusHelper.cs b/RotationSolver.Basic/Helpers/StatusHelper.cs index 8a541155f..cfcc67770 100644 --- a/RotationSolver.Basic/Helpers/StatusHelper.cs +++ b/RotationSolver.Basic/Helpers/StatusHelper.cs @@ -141,7 +141,7 @@ public static class StatusHelper public static bool NeedHealing(this IGameObject p) => p.WillStatusEndGCD(2, 0, false, NoNeedHealingStatus); /// - /// Will any of be end after gcds add seconds? + /// Will any of end after GCDs plus seconds? /// /// /// @@ -153,7 +153,7 @@ public static bool WillStatusEndGCD(this IGameObject obj, uint gcdCount = 0, flo => WillStatusEnd(obj, DataCenter.GCDTime(gcdCount, offset), isFromSelf, statusIDs); /// - /// Will any of be end after seconds? + /// Will any of end after seconds? /// /// /// @@ -169,7 +169,7 @@ public static bool WillStatusEnd(this IGameObject obj, float time, bool isFromSe } /// - /// Please Do NOT use it! + /// Get the remaining time of the status. /// /// /// @@ -179,7 +179,7 @@ public static float StatusTime(this IGameObject obj, bool isFromSelf, params Sta { try { - if (DataCenter.HasApplyStatus(obj.GameObjectId, statusIDs)) return float.MaxValue; + if (obj == null || DataCenter.HasApplyStatus(obj.GameObjectId, statusIDs)) return float.MaxValue; var times = obj.StatusTimes(isFromSelf, statusIDs); if (times == null || !times.Any()) return 0; return Math.Max(0, times.Min() - DataCenter.DefaultGCDRemain); @@ -196,7 +196,7 @@ internal static IEnumerable StatusTimes(this IGameObject obj, bool isFrom } /// - /// Get the stack of the status. + /// Get the stack count of the status. /// /// /// @@ -204,7 +204,7 @@ internal static IEnumerable StatusTimes(this IGameObject obj, bool isFrom /// public static byte StatusStack(this IGameObject obj, bool isFromSelf, params StatusID[] statusIDs) { - if (DataCenter.HasApplyStatus(obj.GameObjectId, statusIDs)) return byte.MaxValue; + if (obj == null || DataCenter.HasApplyStatus(obj.GameObjectId, statusIDs)) return byte.MaxValue; var stacks = obj.StatusStacks(isFromSelf, statusIDs); if (stacks == null || !stacks.Any()) return 0; return stacks.Min(); @@ -216,7 +216,7 @@ private static IEnumerable StatusStacks(this IGameObject obj, bool isFromS } /// - /// Has one status right now. + /// Check if the object has any of the specified statuses. /// /// /// @@ -224,17 +224,17 @@ private static IEnumerable StatusStacks(this IGameObject obj, bool isFromS /// public static bool HasStatus(this IGameObject obj, bool isFromSelf, params StatusID[] statusIDs) { - if (DataCenter.HasApplyStatus(obj.GameObjectId, statusIDs)) return true; + if (obj == null || DataCenter.HasApplyStatus(obj.GameObjectId, statusIDs)) return true; return obj.GetStatus(isFromSelf, statusIDs).Any(); } /// - /// Take the status Off. + /// Remove the specified status. /// /// public static void StatusOff(StatusID status) { - if (!Player.Object?.HasStatus(false, status) ?? true) return; + if (Player.Object == null || !Player.Object.HasStatus(false, status)) return; Chat.Instance.SendMessage($"/statusoff {GetStatusName(status)}"); } @@ -245,7 +245,8 @@ internal static string GetStatusName(StatusID id) private static IEnumerable GetStatus(this IGameObject obj, bool isFromSelf, params StatusID[] statusIDs) { - var newEffects = statusIDs.Select(a => (uint)a); + // Convert statusIDs to a HashSet for faster lookups + var newEffects = new HashSet(statusIDs.Select(a => (uint)a)); var allStatuses = obj.GetAllStatus(isFromSelf); return allStatuses.Where(status => newEffects.Contains(status.StatusId)); } @@ -255,13 +256,15 @@ private static IEnumerable GetAllStatus(this IGameObject obj, bool isFro if (obj is not IBattleChara b) return Enumerable.Empty(); var playerId = Player.Object?.GameObjectId ?? 0; - return b.StatusList.Where(status => !isFromSelf + // Ensure b.StatusList is not null + return b.StatusList?.Where(status => !isFromSelf || status.SourceId == playerId - || status.SourceObject?.OwnerId == playerId); + || status.SourceObject?.OwnerId == playerId) + ?? Enumerable.Empty(); } /// - /// Is status Invincible. + /// Check if the status is invincible. /// /// /// @@ -272,7 +275,7 @@ public static bool IsInvincible(this Status status) } /// - /// Is the status the priority one. + /// Check if the status is a priority. /// /// /// @@ -282,7 +285,7 @@ public static bool IsPriority(this Status status) } /// - /// Is status needs to be dispel immediately. + /// Check if the status needs to be dispelled immediately. /// /// /// @@ -295,7 +298,7 @@ public static bool IsDangerous(this Status status) } /// - /// Can the status be dispel. + /// Check if the status can be dispelled. /// /// /// diff --git a/RotationSolver/Extensions.cs b/RotationSolver/Extensions.cs index 35eb3e1a7..2e8d0ce6d 100644 --- a/RotationSolver/Extensions.cs +++ b/RotationSolver/Extensions.cs @@ -4,7 +4,11 @@ public static class CommandTypeExtensions { public static string ToStateString(this StateCommandType stateType, JobRole role) { - if (stateType == StateCommandType.Auto) + if (DataCenter.IsPvP && stateType == StateCommandType.Auto) + { + return $"{stateType} (LowHP)"; + } + else if (stateType == StateCommandType.Auto) { return $"{stateType} ({DataCenter.TargetingType})"; }