Initial release: HSUI v1.0.0.0 - HUD replacement with configurable hotbars

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-01-30 23:52:46 -05:00
commit f37369cdda
202 changed files with 40137 additions and 0 deletions
+528
View File
@@ -0,0 +1,528 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Statuses;
using HSUI.Config;
using HSUI.Enums;
using HSUI.Helpers;
using HSUI.Interface.Bars;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using BattleChara = Dalamud.Game.ClientState.Objects.Types.IBattleChara;
using BattleNpcSubKind = Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind;
using Character = Dalamud.Game.ClientState.Objects.Types.ICharacter;
namespace HSUI.Interface.GeneralElements
{
public unsafe class UnitFrameHud(UnitFrameConfig config, string displayName)
: DraggableHudElement(config, displayName), IHudElementWithActor, IHudElementWithMouseOver, IHudElementWithPreview, IHudElementWithVisibilityConfig
{
public UnitFrameConfig Config => (UnitFrameConfig)_config;
public VisibilityConfig VisibilityConfig => Config.VisibilityConfig;
private SmoothHPHelper _smoothHPHelper = new SmoothHPHelper();
public IGameObject? Actor { get; set; }
private bool _wasHovering = false;
protected override (List<Vector2>, List<Vector2>) ChildrenPositionsAndSizes()
{
return (new List<Vector2>() { Config.Position }, new List<Vector2>() { Config.Size });
}
public void StopPreview()
{
Config.MouseoverAreaConfig.Preview = false;
Config.SignIconConfig.Preview = false;
}
public void StopMouseover()
{
if (_wasHovering)
{
InputsHelper.Instance.ClearTarget();
_wasHovering = false;
}
}
public override void DrawChildren(Vector2 origin)
{
if (!Config.Enabled || Actor == null)
{
StopMouseover();
return;
}
DrawExtras(origin, Actor);
if (Actor is Character character)
{
DrawCharacter(origin, character);
}
else
{
DrawFriendlyNPC(origin, Actor);
}
// Check if mouse is hovering over the box properly
var startPos = Utils.GetAnchoredPosition(origin + Config.Position, Config.Size, Config.Anchor);
var (areaStart, areaEnd) = Config.MouseoverAreaConfig.GetArea(startPos, Config.Size);
bool isHovering = ImGui.IsMouseHoveringRect(areaStart, areaEnd);
bool ignoreMouseover = Config.MouseoverAreaConfig.Enabled && Config.MouseoverAreaConfig.Ignore;
if (isHovering && !DraggingEnabled)
{
_wasHovering = true;
InputsHelper.Instance.SetTarget(Actor, ignoreMouseover);
if (InputsHelper.Instance.LeftButtonClicked)
{
Plugin.TargetManager.Target = Actor;
}
else if (InputsHelper.Instance.RightButtonClicked)
{
AgentModule.Instance()->GetAgentHUD()->OpenContextMenuFromTarget((GameObject*)Actor.Address);
}
}
else if (_wasHovering)
{
InputsHelper.Instance.ClearTarget();
_wasHovering = false;
}
}
protected virtual void DrawExtras(Vector2 origin, IGameObject? actor)
{
// override
}
private void DrawCharacter(Vector2 pos, Character character)
{
uint currentHp = character.CurrentHp;
uint maxHp = character.MaxHp;
// fixes weird bug with npcs
if (maxHp == 1)
{
currentHp = 1;
}
else if (Config.SmoothHealthConfig.Enabled)
{
currentHp = _smoothHPHelper.GetNextHp((int)currentHp, (int)maxHp, Config.SmoothHealthConfig.Velocity);
}
PluginConfigColor fillColor = ColorUtils.ColorForCharacter(
character,
currentHp,
maxHp,
Config.UseJobColor,
Config.UseRoleColor,
Config.ColorByHealth
) ?? Config.FillColor;
Rect background = new Rect(Config.Position, Config.Size, BackgroundColor(character));
if (Config.RangeConfig.Enabled || Config.EnemyRangeConfig.Enabled)
{
fillColor = GetDistanceColor(character, fillColor);
background.Color = GetDistanceColor(character, background.Color);
}
Rect healthFill = BarUtilities.GetFillRect(Config.Position, Config.Size, Config.FillDirection, fillColor, currentHp, maxHp);
BarHud bar = new BarHud(Config, character);
bar.NeedsInputs = true;
bar.SetBackground(background);
bar.AddForegrounds(healthFill);
bar.AddLabels(GetLabels(maxHp));
if (Config.UseMissingHealthBar)
{
Vector2 healthMissingSize = Config.Size - BarUtilities.GetFillDirectionOffset(healthFill.Size, Config.FillDirection);
Vector2 healthMissingPos = Config.FillDirection.IsInverted()
? Config.Position
: Config.Position + BarUtilities.GetFillDirectionOffset(healthFill.Size, Config.FillDirection);
PluginConfigColor missingHealthColor = Config.UseJobColorAsMissingHealthColor && character is BattleChara
? GlobalColors.Instance.SafeColorForJobId(character!.ClassJob.RowId)
: Config.UseRoleColorAsMissingHealthColor && character is BattleChara
? GlobalColors.Instance.SafeRoleColorForJobId(character!.ClassJob.RowId)
: Config.HealthMissingColor;
if (Config.UseDeathIndicatorBackgroundColor && character is BattleChara { CurrentHp: <= 0 })
{
missingHealthColor = Config.DeathIndicatorBackgroundColor;
}
if (Config.UseCustomInvulnerabilityColor && character is BattleChara battleChara)
{
IStatus? tankInvuln = Utils.GetTankInvulnerabilityID(battleChara);
if (tankInvuln is not null)
{
missingHealthColor = Config.CustomInvulnerabilityColor;
}
}
if (Config.RangeConfig.Enabled || Config.EnemyRangeConfig.Enabled)
{
missingHealthColor = GetDistanceColor(character, missingHealthColor);
}
bar.AddForegrounds(new Rect(healthMissingPos, healthMissingSize, missingHealthColor));
}
// shield
BarUtilities.AddShield(bar, Config, Config.ShieldConfig, character, healthFill.Size);
// draw action
AddDrawActions(bar.GetDrawActions(pos, Config.StrataLevel));
// mouseover area
BarHud? mouseoverAreaBar = Config.MouseoverAreaConfig.GetBar(
Config.Position,
Config.Size,
Config.ID + "_mouseoverArea",
Config.Anchor
);
if (mouseoverAreaBar != null)
{
AddDrawActions(mouseoverAreaBar.GetDrawActions(pos, StrataLevel.HIGHEST));
}
// role/job icon
if (Config.RoleIconConfig.Enabled && character is IPlayerCharacter)
{
uint jobId = character.ClassJob.RowId;
uint iconId = Config.RoleIconConfig.UseRoleIcons ?
JobsHelper.RoleIconIDForJob(jobId, Config.RoleIconConfig.UseSpecificDPSRoleIcons) :
JobsHelper.IconIDForJob(jobId, (uint)Config.RoleIconConfig.Style);
if (iconId > 0)
{
var barPos = Utils.GetAnchoredPosition(pos, Config.Size, Config.Anchor);
var parentPos = Utils.GetAnchoredPosition(barPos + Config.Position, -Config.Size, Config.RoleIconConfig.FrameAnchor);
var iconPos = Utils.GetAnchoredPosition(parentPos + Config.RoleIconConfig.Position, Config.RoleIconConfig.Size, Config.RoleIconConfig.Anchor);
AddDrawAction(Config.RoleIconConfig.StrataLevel, () =>
{
DrawHelper.DrawInWindow(ID + "_jobIcon", iconPos, Config.RoleIconConfig.Size, false, (drawList) =>
{
DrawHelper.DrawIcon(iconId, iconPos, Config.RoleIconConfig.Size, false, drawList);
});
});
}
}
// sign icon
if (Config.SignIconConfig.Enabled)
{
uint? iconId = Config.SignIconConfig.IconID(character);
if (iconId.HasValue)
{
var barPos = Utils.GetAnchoredPosition(pos, Config.Size, Config.Anchor);
var parentPos = Utils.GetAnchoredPosition(barPos + Config.Position, -Config.Size, Config.SignIconConfig.FrameAnchor);
var iconPos = Utils.GetAnchoredPosition(parentPos + Config.SignIconConfig.Position, Config.SignIconConfig.Size, Config.SignIconConfig.Anchor);
AddDrawAction(Config.SignIconConfig.StrataLevel, () =>
{
DrawHelper.DrawInWindow(ID + "_signIcon", iconPos, Config.SignIconConfig.Size, false, (drawList) =>
{
DrawHelper.DrawIcon(iconId.Value, iconPos, Config.SignIconConfig.Size, false, drawList);
});
});
}
}
}
private LabelConfig[] GetLabels(uint maxHp)
{
List<LabelConfig> labels = new List<LabelConfig>();
if (Config.HideHealthIfPossible && maxHp <= 1)
{
if (!Utils.IsHealthLabel(Config.LeftLabelConfig))
{
labels.Add(Config.LeftLabelConfig);
}
if (!Utils.IsHealthLabel(Config.RightLabelConfig))
{
labels.Add(Config.RightLabelConfig);
}
if (!Utils.IsHealthLabel(Config.OptionalLabelConfig))
{
labels.Add(Config.OptionalLabelConfig);
}
}
else
{
labels.Add(Config.LeftLabelConfig);
labels.Add(Config.RightLabelConfig);
labels.Add(Config.OptionalLabelConfig);
}
return labels.ToArray();
}
private PluginConfigColor GetDistanceColor(Character? character, PluginConfigColor color)
{
byte distance = character != null ? character.YalmDistanceX : byte.MaxValue;
float currentAlpha = color.Vector.W * 100f;
float alpha = Config.RangeConfig.AlphaForDistance(distance, currentAlpha) / 100f;
if (character is IBattleNpc { BattleNpcKind: BattleNpcSubKind.Enemy or BattleNpcSubKind.BattleNpcPart } && Config.EnemyRangeConfig.Enabled)
{
alpha = Config.EnemyRangeConfig.AlphaForDistance(distance, currentAlpha) / 100f;
}
return color.WithAlpha(alpha);
}
private unsafe void GetNPCHpValues(IGameObject? actor, out uint currentHp, out uint maxHp)
{
currentHp = 0;
maxHp = 0;
var player = Plugin.ObjectTable.LocalPlayer;
if (player == null || actor == null || player.TargetObject == null || actor.GameObjectId != player.TargetObject.GameObjectId)
{
return;
}
AtkUnitBase* TargetWidget = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_TargetInfoMainTarget", 1).Address;
if (TargetWidget != null)
{
AtkTextNode* textNode = TargetWidget->GetTextNodeById(11);
string integrityText = textNode->NodeText.ToString();
// not a gathering node or node at 100%, nothing to do
if (!integrityText.Contains("%"))
{
return;
}
try
{
currentHp = Convert.ToUInt32((integrityText.Replace("%", "")));
maxHp = 100;
}
catch { }
}
}
private void DrawFriendlyNPC(Vector2 pos, IGameObject? actor)
{
GetNPCHpValues(actor, out uint currentHp, out uint maxHp);
BarHud bar = new BarHud(Config, actor);
bar.AddLabels(GetLabels(0));
if (maxHp == 0)
{
bar.AddForegrounds(new Rect(Config.Position, Config.Size, ColorUtils.ColorForActor(actor)));
}
else
{
if (Config.SmoothHealthConfig.Enabled)
{
currentHp = _smoothHPHelper.GetNextHp((int)currentHp, (int)maxHp, Config.SmoothHealthConfig.Velocity);
}
PluginConfigColor fillColor = ColorUtils.ColorForCharacter(
actor,
currentHp,
maxHp,
colorByHealthConfig: Config.ColorByHealth
) ?? Config.FillColor;
Rect background = new Rect(Config.Position, Config.Size, Config.BackgroundColor);
Rect healthFill = BarUtilities.GetFillRect(Config.Position, Config.Size, Config.FillDirection, fillColor, currentHp, maxHp);
bar.NeedsInputs = true;
bar.SetBackground(background);
bar.AddForegrounds(healthFill);
}
AddDrawActions(bar.GetDrawActions(pos, Config.StrataLevel));
}
private PluginConfigColor BackgroundColor(Character? chara)
{
if (Config.ShowTankInvulnerability &&
!Config.UseMissingHealthBar &&
chara is BattleChara battleChara)
{
IStatus? tankInvuln = Utils.GetTankInvulnerabilityID(battleChara);
if (tankInvuln != null)
{
PluginConfigColor color;
if (Config.UseCustomInvulnerabilityColor)
{
color = Config.CustomInvulnerabilityColor;
}
else if (tankInvuln.StatusId == 811 && Config.UseCustomWalkingDeadColor)
{
color = Config.CustomWalkingDeadColor;
}
else
{
color = new PluginConfigColor(GlobalColors.Instance.SafeColorForJobId(chara.ClassJob.RowId).Vector.AdjustColor(-.8f));
}
return color;
}
}
if (chara is BattleChara)
{
if (Config.UseJobColorAsBackgroundColor)
{
return GlobalColors.Instance.SafeColorForJobId(chara.ClassJob.RowId);
}
else if (Config.UseRoleColorAsBackgroundColor)
{
return GlobalColors.Instance.SafeRoleColorForJobId(chara.ClassJob.RowId);
}
else if (Config.UseDeathIndicatorBackgroundColor && chara.CurrentHp <= 0)
{
return Config.DeathIndicatorBackgroundColor;
}
else
{
return Config.BackgroundColor;
}
}
return GlobalColors.Instance.EmptyUnitFrameColor;
}
}
public class PlayerUnitFrameHud : UnitFrameHud
{
public new PlayerUnitFrameConfig Config => (PlayerUnitFrameConfig)_config;
public PlayerUnitFrameHud(PlayerUnitFrameConfig config, string displayName) : base(config, displayName)
{
}
protected override void DrawExtras(Vector2 origin, IGameObject? actor)
{
TankStanceIndicatorConfig config = Config.TankStanceIndicatorConfig;
if (!config.Enabled || actor is not IPlayerCharacter chara) { return; }
uint jobId = chara.ClassJob.RowId;
if (JobsHelper.RoleForJob(jobId) != JobRoles.Tank) { return; }
var tankStanceBuff = Utils.StatusListForBattleChara(chara).Where(o =>
o.StatusId == 79 || // IRON WILL
o.StatusId == 91 || // DEFIANCE
o.StatusId == 392 || // ROYAL GUARD
o.StatusId == 393 || // IRON WILL
o.StatusId == 743 || // GRIT
o.StatusId == 1396 || // DEFIANCE
o.StatusId == 1397 || // GRIT
o.StatusId == 1833 // ROYAL GUARD
);
PluginConfigColor color = tankStanceBuff.Any() ? config.ActiveColor : config.InactiveColor;
Vector2 pos = GetTankStanceCornerOrigin(origin);
var (verticalDir, horizontalDir) = GetTankStanceLinesDirections();
pos = new Vector2(pos.X + config.Thickess * -horizontalDir, pos.Y + config.Thickess * -verticalDir);
Vector2 vSize = new Vector2(config.Thickess * horizontalDir, (config.Size.Y + config.Thickess) * verticalDir);
Vector2 vEndPos = pos + vSize;
Vector2 hSize = new Vector2((config.Size.X + config.Thickess) * horizontalDir, config.Thickess * verticalDir);
Vector2 hEndPos = pos + hSize;
Vector2 startPos = new Vector2(Math.Min(pos.X, hEndPos.X), Math.Min(pos.Y, hEndPos.Y));
Vector2 endPos = new Vector2(Math.Max(pos.X, hEndPos.X), Math.Max(pos.Y, hEndPos.Y)); ;
AddDrawAction(StrataLevel.LOWEST, () =>
{
DrawHelper.DrawInWindow(ID + "_TankStance", startPos, endPos - startPos, false, (drawList) =>
{
// TODO: clean up hacky math
// there's some 1px errors prob due to negative sizes
// couldn't figure it out so I did the hacky fixes
// vertical
drawList.AddRectFilled(pos, vEndPos, color.Base);
if (config.Corner == TankStanceCorner.TopRight)
{
drawList.AddLine(pos, pos + new Vector2(0, vSize.Y + 1), 0xFF000000);
}
else
{
drawList.AddLine(pos, pos + new Vector2(0, vSize.Y), 0xFF000000);
}
drawList.AddLine(pos + vSize, pos + vSize + new Vector2(-vSize.X, 0), 0xFF000000);
// horizontal
drawList.AddRectFilled(pos, hEndPos, color.Base);
if (config.Corner == TankStanceCorner.BottomLeft)
{
drawList.AddLine(pos, pos + new Vector2(hSize.X + 1, 0), 0xFF000000);
}
else
{
drawList.AddLine(pos, pos + new Vector2(hSize.X, 0), 0xFF000000);
}
if (config.Corner == TankStanceCorner.BottomRight)
{
drawList.AddLine(pos + new Vector2(0, 1), pos + new Vector2(0, hSize.Y), 0xFF000000);
}
else
{
drawList.AddLine(pos, pos + new Vector2(0, hSize.Y), 0xFF000000);
}
drawList.AddLine(pos + hSize, pos + hSize + new Vector2(0, -hSize.Y), 0xFF000000);
});
});
}
private Vector2 GetTankStanceCornerOrigin(Vector2 origin)
{
var topLeft = Utils.GetAnchoredPosition(origin + Config.Position, Config.Size, Config.Anchor);
return Config.TankStanceIndicatorConfig.Corner switch
{
TankStanceCorner.TopRight => topLeft + new Vector2(Config.Size.X - 1, 0),
TankStanceCorner.BottomLeft => topLeft + new Vector2(0, Config.Size.Y - 1),
TankStanceCorner.BottomRight => topLeft + Config.Size - Vector2.One,
_ => topLeft
};
}
private (int, int) GetTankStanceLinesDirections()
{
return Config.TankStanceIndicatorConfig.Corner switch
{
TankStanceCorner.TopLeft => (1, 1),
TankStanceCorner.TopRight => (1, -1),
TankStanceCorner.BottomLeft => (-1, 1),
_ => (-1, -1)
};
}
}
}