80f45f5a31
Co-authored-by: Cursor <cursoragent@cursor.com>
451 lines
17 KiB
C#
451 lines
17 KiB
C#
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;
|
||
}
|
||
|
||
/// <summary>Returns Grand Company icon ID (Maelstrom, Flames, or Adders) for enemy players in PvP Frontline.
|
||
/// Nameplate color types 4, 5, 6 map to the three teams. Returns null when not a PvP enemy team (4–6).
|
||
/// Use iconOverrides for custom IDs: [0]=team1(plate4), [1]=team2(plate5), [2]=team3(plate6). Non-zero overrides default.</summary>
|
||
public static unsafe uint? GrandCompanyIconIdForPvPEnemy(IGameObject? obj, (int t1, int t2, int t3)? iconOverrides = null)
|
||
{
|
||
if (obj == null || !Plugin.ClientState.IsPvP) return null;
|
||
StructsGameObject* gameObject = (StructsGameObject*)obj.Address;
|
||
byte plateType = gameObject->GetNamePlateColorType();
|
||
if (plateType < 4 || plateType > 6) return null;
|
||
|
||
// 62601=Maelstrom, 62602=Twin Adder, 62603=Immortal Flames
|
||
uint iconId = plateType switch
|
||
{
|
||
4 => iconOverrides.HasValue && iconOverrides.Value.t1 > 0 ? (uint)iconOverrides.Value.t1 : 62601u,
|
||
5 => iconOverrides.HasValue && iconOverrides.Value.t2 > 0 ? (uint)iconOverrides.Value.t2 : 62602u,
|
||
6 => iconOverrides.HasValue && iconOverrides.Value.t3 > 0 ? (uint)iconOverrides.Value.t3 : 62603u,
|
||
_ => 0
|
||
};
|
||
return iconId > 0 ? iconId : null;
|
||
}
|
||
|
||
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<IStatus> StatusListForActor(IGameObject? obj)
|
||
{
|
||
if (obj is IBattleChara chara)
|
||
{
|
||
return StatusListForBattleChara(chara);
|
||
}
|
||
|
||
return new List<IStatus>();
|
||
}
|
||
|
||
public static IEnumerable<IStatus> StatusListForBattleChara(IBattleChara? chara)
|
||
{
|
||
List<IStatus> statusList = new List<IStatus>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the actual target ID of your targets target.
|
||
/// </summary>
|
||
/// <param name="target">Your target</param>
|
||
/// <returns>Target ID of your targets targer. Returns -1 if old code should be ran.</returns>
|
||
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");
|
||
}
|
||
}
|
||
}
|