Files
HSUI/Helpers/Utils.cs
T

451 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (46).
/// 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");
}
}
}