Initial release: HSUI v1.0.0.0 - HUD replacement with configurable hotbars
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,429 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user