using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Statuses; using Dalamud.Plugin.Services; using HSUI.Config; using HSUI.Enums; using HSUI.Interface.GeneralElements; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Component.GUI; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using StructsCharacter = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; using StructsCharacterManager = FFXIVClientStructs.FFXIV.Client.Game.Character.CharacterManager; using StructsGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; namespace HSUI.Helpers { internal static class Utils { private static uint InvalidGameObjectId = 0xE0000000; public static IGameObject? GetBattleChocobo(IGameObject? player) { if (player == null) { return null; } return GetBuddy(player.GameObjectId, BattleNpcSubKind.Chocobo); } public static IGameObject? GetBuddy(ulong ownerId, BattleNpcSubKind kind) { // only the first 200 elements in the array are relevant due to the order in which SE packs data into the array // we do a step of 2 because its always an actor followed by its companion for (var i = 0; i < 200; i += 2) { var gameObject = Plugin.ObjectTable[i]; if (gameObject == null || gameObject.GameObjectId == InvalidGameObjectId || gameObject is not IBattleNpc battleNpc) { continue; } if (battleNpc.BattleNpcKind == kind && battleNpc.OwnerId == ownerId) { return gameObject; } } return null; } public static IGameObject? GetGameObjectByName(string name) { // only the first 200 elements in the array are relevant due to the order in which SE packs data into the array // we do a step of 2 because its always an actor followed by its companion for (int i = 0; i < 200; i += 2) { IGameObject? gameObject = Plugin.ObjectTable[i]; if (gameObject == null || gameObject.GameObjectId == InvalidGameObjectId || gameObject.GameObjectId == 0) { continue; } if (gameObject.Name.ToString() == name) { return gameObject; } } return null; } public static unsafe bool IsHostile(IGameObject obj) { StructsGameObject* gameObject = (StructsGameObject*)obj.Address; byte plateType = gameObject->GetNamePlateColorType(); // 4, 5, 6: Enemy players in PvP // 7: yellow, can be attacked, not engaged // 8: dead // 9: red, engaged with your party // 10: purple, engaged with other party // 11: orange, aggro'd to your party but not attacked yet return plateType >= 4 && plateType <= 11; } public static unsafe float ActorShieldValue(IGameObject? actor) { if (actor == null || actor is not ICharacter) { return 0f; } StructsCharacter* chara = (StructsCharacter*)actor.Address; return Math.Min(chara->CharacterData.ShieldValue, 100f) / 100f; } public static bool IsActorCasting(IGameObject? actor) { if (actor is not IBattleChara chara) { return false; } try { return chara.IsCasting; } catch { } return false; } public static IEnumerable StatusListForActor(IGameObject? obj) { if (obj is IBattleChara chara) { return StatusListForBattleChara(chara); } return new List(); } public static IEnumerable StatusListForBattleChara(IBattleChara? chara) { List statusList = new List(); if (chara == null) { return statusList; } try { statusList = chara.StatusList.ToList(); } catch { } return statusList; } public static string DurationToString(double duration, int decimalCount = 0) { if (duration == 0) { return ""; } TimeSpan t = TimeSpan.FromSeconds(duration); if (t.Hours >= 1) { return t.Hours + "h"; } if (t.Minutes >= 5) { return t.Minutes + "m"; } if (t.Minutes >= 1) { return $"{t.Minutes}:{t.Seconds:00}"; } return duration.ToString("N" + decimalCount, ConfigurationManager.Instance.ActiveCultreInfo); } public static IStatus? GetTankInvulnerabilityID(IBattleChara actor) { return StatusListForBattleChara(actor).FirstOrDefault(o => o.StatusId is 810 or 811 or 3255 or 1302 or 409 or 1836 or 82); } public static bool IsOnCleanseJob() { IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer; return player != null && JobsHelper.IsJobWithCleanse(player.ClassJob.RowId, player.Level); } public static IGameObject? FindTargetOfTarget(IGameObject? target, IGameObject? player, IObjectTable actors) { if (target == null) { return null; } // Dalamud for now has an issue where it is only able to get the target ID of // NON-Networked objects through anything but GetTargetId on ClientStruct Gameobjects. // The bypass converts all Dalamud GameObject Data to ClientStructs GameObject Data and handles it accordingly. int actualTargetId = GetActualTargetId(target); // The Object ID that gets returned from minions is in reality the index // Checking for the correct object ID wouldn't work anyways as you would yet again run into the ObjectID = 0xE0000000 issue if (actualTargetId >= 0 && actualTargetId < actors.Length) { return actors[actualTargetId]; } if (target.TargetObjectId == 0 && player != null && player.TargetObjectId == 0) { return player; } // only the first 200 elements in the array are relevant due to the order in which SE packs data into the array // we do a step of 2 because its always an actor followed by its companion for (int i = 0; i < 200; i += 2) { IGameObject? actor = actors[i]; if (actor?.GameObjectId == target.TargetObjectId) { return actor; } } return null; } /// /// Gets the actual target ID of your targets target. /// /// Your target /// Target ID of your targets targer. Returns -1 if old code should be ran. private static unsafe int GetActualTargetId(IGameObject target) { // We only need to check for companions. // Why not check target.TargetObject?.ObjectKind == ObjectKind.Companion? // Due to the Non-Networked game object bug the game is unaware of what type the object should actually be if (target.TargetObject?.ObjectKind != ObjectKind.Player) { return -1; } // Here we get the ClientStruct Character of our target (aka the player we are targeting) StructsCharacter targetChara = StructsCharacterManager.Instance()->LookupBattleCharaByEntityId(target.EntityId)->Character; // This method is key. GetTargetId() returns the targets player target ID. If it is converted to a hex string and starts with the number 4, it is a minion. // Even though it is a minion, it still returns the players target ID. ulong realTargetID = targetChara.GetTargetId(); if (!realTargetID.ToString("X").StartsWith("4")) { return -1; } // We look up the parents ClientStruct GameObject StructsCharacter* realBattleChara = (StructsCharacter*)StructsCharacterManager.Instance()->LookupBattleCharaByEntityId((uint)realTargetID); if (realBattleChara == null) { return -1; } // And get the companion off of that StructsGameObject* companionGameObject = (StructsGameObject*)realBattleChara->CompanionData.CompanionObject; if (companionGameObject == null) { return -1; } // We return the index of the object here. Why? // Again due to the bug where ObjectID = 0xE0000000 // The index does work and returns the exact minion index. return companionGameObject->ObjectIndex; } public static Vector2 GetAnchoredPosition(Vector2 position, Vector2 size, DrawAnchor anchor) { return anchor switch { DrawAnchor.Center => position - size / 2f, DrawAnchor.Left => position + new Vector2(0, -size.Y / 2f), DrawAnchor.Right => position + new Vector2(-size.X, -size.Y / 2f), DrawAnchor.Top => position + new Vector2(-size.X / 2f, 0), DrawAnchor.TopLeft => position, DrawAnchor.TopRight => position + new Vector2(-size.X, 0), DrawAnchor.Bottom => position + new Vector2(-size.X / 2f, -size.Y), DrawAnchor.BottomLeft => position + new Vector2(0, -size.Y), DrawAnchor.BottomRight => position + new Vector2(-size.X, -size.Y), _ => position }; } public static string UserFriendlyConfigName(string configTypeName) => UserFriendlyString(configTypeName, "Config"); public static string UserFriendlyString(string str, string? remove) { string? s = remove != null ? str.Replace(remove, "") : str; Regex? regex = new(@" (?<=[A-Z])(?=[A-Z][a-z]) | (?<=[^A-Z])(?=[A-Z]) | (?<=[A-Za-z])(?=[^A-Za-z])", RegexOptions.IgnorePatternWhitespace); return regex.Replace(s, " "); } public static void OpenUrl(string url) { try { Process.Start(url); } catch { try { // hack because of this: https://github.com/dotnet/corefx/issues/10361 if (RuntimeInformation.IsOSPlatform(osPlatform: OSPlatform.Windows)) { url = url.Replace("&", "^&"); Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true }); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { Process.Start("xdg-open", url); } } catch (Exception e) { Plugin.Logger.Error("Error trying to open url: " + e.Message); } } } public static unsafe bool? IsTargetCasting() { AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_TargetInfo", 1).Address; if (addon != null && addon->IsVisible) { AtkImageNode* imageNode = addon->GetImageNodeById(15); return imageNode == null || imageNode->IsVisible(); } addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_TargetInfoCastBar", 1).Address; if (addon != null && addon->IsVisible) { AtkImageNode* imageNode = addon->GetImageNodeById(7); return imageNode != null || imageNode->IsVisible(); } return null; } public static unsafe bool? IsFocusTargetCasting() { AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_FocusTargetInfo", 1).Address; if (addon != null && addon->IsVisible) { AtkTextNode* textNode = addon->GetTextNodeById(5); return textNode == null || textNode->IsVisible(); } return null; } public static unsafe bool? IsEnemyInListCasting(int index) { if (index < 0 || index > 7) { return null; } AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_EnemyList", 1).Address; if (addon == null || !addon->IsVisible) { return null; } uint buttonId = (index == 0) ? 2u : (uint)(20000 + index); AtkComponentButton* button = addon->GetComponentButtonById(buttonId); if (button == null || button->AtkResNode == null || !button->AtkResNode->IsVisible()) { return false; } AtkImageNode* imageNode = button->GetImageNodeById(8); return imageNode == null || imageNode->IsVisible(); } public static unsafe uint? SignIconIDForActor(IGameObject? actor) { if (actor == null) { return null; } return SignIconIDForObjectID(actor.GameObjectId); } public static unsafe uint? SignIconIDForObjectID(ulong objectId) { MarkingController* markingController = MarkingController.Instance(); if (objectId == 0 || objectId == InvalidGameObjectId || markingController == null) { return null; } for (int i = 0; i < 17; i++) { if (objectId == markingController->Markers[i]) { // attack1-5 if (i <= 4) { return (uint)(61201 + i); } // attack6-8 else if (i >= 14) { return (uint)(61201 + i - 9); } // shapes else if (i >= 10) { return (uint)(61231 + i - 10); } // ignore1-2 else if (i >= 8) { return (uint)(61221 + i - 8); } // bind1-3 else if (i >= 5) { return (uint)(61211 + i - 5); } } } return null; } public static bool IsHealthLabel(LabelConfig config) { return config.GetText().Contains("[health"); } } }