Files
HSUI/Helpers/Utils.cs
T

430 lines
15 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;
}
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");
}
}
}