Files
HSUI/Interface/HudHelper.cs
T
KnackAtNite 95c42af5b8 Combo highlight config, tooltips, nameplates, hotbars fixes
- Combo highlight: configurable color, glow, line style (solid/dashed/dotted), thickness
- Tooltips: font selection, scaling slider, improved wrap/cramping handling
- Nameplates: custom quest icons with config, position smoothing fix for jitter
- Hotbars: hide keybinds on empty slots, combo highlight within icon bounds
- HudHelper: restore default nameplates on plugin disable

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-01-31 02:05:30 -05:00

526 lines
20 KiB
C#

using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.SubKinds;
using HSUI.Config;
using HSUI.Helpers;
using HSUI.Interface.EnemyList;
using HSUI.Interface.GeneralElements;
using HSUI.Interface.Party;
using HSUI.Interface.Jobs;
using HSUI.Interface.Nameplates;
using HSUI.Interface.StatusEffects;
using FFXIVClientStructs.FFXIV.Component.GUI;
using System;
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
namespace HSUI.Interface
{
public class HudHelper : IDisposable
{
private HUDOptionsConfig Config => ConfigurationManager.Instance.GetConfigObject<HUDOptionsConfig>();
private bool _firstUpdate = true;
private Vector2 _castBarPos = Vector2.Zero;
private bool _hidingCastBar = false;
private Vector2 _pullTimerPos = Vector2.Zero;
private bool _hidingPullTimer = false;
public HudHelper()
{
Config.ValueChangeEvent += ConfigValueChanged;
}
~HudHelper()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected void Dispose(bool disposing)
{
if (!disposing)
{
return;
}
Config.ValueChangeEvent -= ConfigValueChanged;
void RestoreDefaults()
{
try
{
SetGameHudElementsHidden(Array.Empty<uint>(), true);
UpdateDefaultCastBar(true);
UpdateDefaultPulltimer(true);
UpdateDefaultNameplates(true);
UpdateJobGauges(true);
}
catch (Exception ex)
{
Plugin.Logger.Error($"Exception during HudHelper.Dispose restore: {ex.Message}");
}
}
if (Plugin.Framework.IsInFrameworkUpdateThread && Plugin.ObjectTable.LocalPlayer != null)
{
RestoreDefaults();
}
else
{
try
{
Plugin.Framework.RunOnFrameworkThread(() =>
{
if (Plugin.ObjectTable.LocalPlayer != null)
RestoreDefaults();
});
}
catch (Exception ex)
{
Plugin.Logger.Error($"Exception scheduling HudHelper.Dispose restore: {ex.Message}");
}
}
}
public void Update()
{
try
{
if (_firstUpdate)
{
_firstUpdate = false;
UpdateDefaultCastBar();
UpdateDefaultPulltimer();
}
UpdateJobGauges();
UpdateDefaultHudElementsHidden();
UpdateDefaultNameplates();
}
catch (Exception ex)
{
var now = DateTime.UtcNow;
if ((now - _lastHudElementsErrorLog).TotalSeconds >= HudElementsErrorLogIntervalSeconds)
{
_lastHudElementsErrorLog = now;
Plugin.Logger.Warning($"[HSUI] HudHelper.Update: {ex.Message}");
}
}
}
public bool IsElementHidden(HudElement element)
{
IHudElementWithVisibilityConfig? e = element as IHudElementWithVisibilityConfig;
if (e == null || e.VisibilityConfig == null) { return false; }
return !e.VisibilityConfig.IsElementVisible(element);
}
private void ConfigValueChanged(object sender, OnChangeBaseArgs e)
{
switch (e.PropertyName)
{
case "HideDefaultHudWhenReplaced":
UpdateDefaultHudElementsHidden();
break;
case "HideDefaultCastbar":
UpdateDefaultCastBar();
break;
case "HideDefaultPulltimer":
UpdateDefaultPulltimer();
break;
}
}
private static DateTime _lastHudElementsErrorLog = DateTime.MinValue;
private const double HudElementsErrorLogIntervalSeconds = 10.0;
private void UpdateDefaultHudElementsHidden()
{
try
{
UpdateDefaultHudElementsHiddenCore();
}
catch (Exception ex)
{
var now = DateTime.UtcNow;
if ((now - _lastHudElementsErrorLog).TotalSeconds >= HudElementsErrorLogIntervalSeconds)
{
_lastHudElementsErrorLog = now;
Plugin.Logger.Warning($"[HSUI] HudHelper: skipped HUD hide update (resolver not ready): {ex.Message}");
}
}
}
private void UpdateDefaultHudElementsHiddenCore()
{
if (Plugin.Condition.Any(
ConditionFlag.OccupiedInEvent,
ConditionFlag.OccupiedInQuestEvent,
ConditionFlag.OccupiedInCutSceneEvent,
ConditionFlag.OccupiedSummoningBell,
ConditionFlag.Occupied,
ConditionFlag.Occupied30,
ConditionFlag.Occupied33,
ConditionFlag.Occupied38,
ConditionFlag.Occupied39,
ConditionFlag.WatchingCutscene,
ConditionFlag.WatchingCutscene78,
ConditionFlag.CreatingCharacter,
ConditionFlag.BetweenAreas,
ConditionFlag.BetweenAreas51,
ConditionFlag.BoundByDuty95,
ConditionFlag.ChocoboRacing,
ConditionFlag.PlayingLordOfVerminion)
|| Plugin.ClientState.IsPvP)
{
return;
}
if (!Config.HideDefaultHudWhenReplaced)
{
SetGameHudElementsHidden(Array.Empty<uint>(), false);
return;
}
var hashesToHide = new List<uint>();
void AddHashes(params string[] addonNames)
{
foreach (var name in addonNames)
{
var h = HudLayoutHashHelper.GetHash(name);
if (h != 0)
hashesToHide.Add(h);
}
}
var hotbarsConfig = ConfigurationManager.Instance?.GetConfigObject<HotbarsConfig>();
if (hotbarsConfig?.Enabled == true)
{
AddHashes("_ActionBar", "_ActionBar01", "_ActionBar02", "_ActionBar03", "_ActionBar04",
"_ActionBar05", "_ActionBar06", "_ActionBar07", "_ActionBar08", "_ActionBar09", "_ActionCross");
}
var playerUnitFrame = ConfigurationManager.Instance?.GetConfigObject<PlayerUnitFrameConfig>();
if (playerUnitFrame?.Enabled == true)
AddHashes("_ParameterWidget");
var targetUnitFrame = ConfigurationManager.Instance?.GetConfigObject<TargetUnitFrameConfig>();
if (targetUnitFrame?.Enabled == true)
AddHashes("_TargetInfo", "_TargetInfoMainTarget", "_TargetInfoCastBar", "_TargetInfoBuffDebuff");
var focusUnitFrame = ConfigurationManager.Instance?.GetConfigObject<FocusTargetUnitFrameConfig>();
if (focusUnitFrame?.Enabled == true)
AddHashes("_FocusTargetInfo");
var playerCastbar = ConfigurationManager.Instance?.GetConfigObject<PlayerCastbarConfig>();
if (playerCastbar?.Enabled == true)
AddHashes("_CastBar");
var expBar = ConfigurationManager.Instance?.GetConfigObject<ExperienceBarConfig>();
if (expBar?.Enabled == true)
AddHashes("_Exp");
var limitBreak = ConfigurationManager.Instance?.GetConfigObject<LimitBreakConfig>();
if (limitBreak?.Enabled == true)
AddHashes("_LimitBreak");
var playerBuffs = ConfigurationManager.Instance?.GetConfigObject<PlayerBuffsListConfig>();
var playerDebuffs = ConfigurationManager.Instance?.GetConfigObject<PlayerDebuffsListConfig>();
if (playerBuffs?.Enabled == true || playerDebuffs?.Enabled == true)
AddHashes("_Status", "_StatusCustom0", "_StatusCustom1", "_StatusCustom2", "_StatusCustom3");
var partyFrames = ConfigurationManager.Instance?.GetConfigObject<PartyFramesConfig>();
if (partyFrames?.Enabled == true)
AddHashes("_PartyList");
var enemyList = ConfigurationManager.Instance?.GetConfigObject<EnemyListConfig>();
if (enemyList?.Enabled == true)
AddHashes("_EnemyList");
// Hide NamePlate when HSUI nameplates are enabled. We read icon IDs from the addon (still updated when hidden)
// and draw our own quest icons (! ? above NPCs) via NPC nameplate IconConfig.
var nameplatesConfig = ConfigurationManager.Instance?.GetConfigObject<NameplatesGeneralConfig>();
if (nameplatesConfig?.Enabled == true)
AddHashes("NamePlate");
if (IsCurrentJobHudEnabled())
{
foreach (var name in GetCurrentJobGaugeAddonNames())
AddHashes(name);
}
SetGameHudElementsHidden(hashesToHide.ToArray(), false);
}
/// <summary>Visibility (ByteValue2) of game HUD elements before we hid them. Restored on disable/unload.</summary>
private readonly Dictionary<uint, byte> _gameHudVisibilityBeforeHide = new();
private unsafe void SetGameHudElementsHidden(uint[] hashesToHide, bool forceRestore)
{
AddonConfig* config = AddonConfig.Instance();
if (config == null || !config->IsLoaded || config->ActiveDataSet == null)
return;
var hashSet = new HashSet<uint>(hashesToHide);
Span<AddonConfigEntry> entries = config->ActiveDataSet->HudLayoutConfigEntries;
bool hasChanges = false;
if (forceRestore || hashesToHide.Length == 0)
{
foreach (ref var entry in entries)
{
if (!_gameHudVisibilityBeforeHide.TryGetValue(entry.AddonNameHash, out byte saved))
continue;
if (entry.ByteValue2 == saved)
continue;
entry.ByteValue2 = saved;
hasChanges = true;
}
_gameHudVisibilityBeforeHide.Clear();
}
else
{
foreach (ref var entry in entries)
{
if (!hashSet.Contains(entry.AddonNameHash))
continue;
if (!_gameHudVisibilityBeforeHide.ContainsKey(entry.AddonNameHash))
_gameHudVisibilityBeforeHide[entry.AddonNameHash] = entry.ByteValue2;
if (entry.ByteValue2 == 0x0)
continue;
entry.ByteValue2 = 0x0;
hasChanges = true;
}
}
if (hasChanges)
{
config->SaveFile(true);
config->ApplyHudLayout();
}
}
private unsafe void UpdateDefaultCastBar(bool forceVisible = false)
{
if (Config.HideDefaultCastbar && !_hidingCastBar)
{
Plugin.AddonLifecycle.RegisterListener(AddonEvent.PreDraw, "_CastBar", (addonEvent, args) =>
{
AtkUnitBase* addon = (AtkUnitBase*)args.Addon.Address;
if (!_hidingCastBar)
{
_castBarPos = new Vector2(addon->RootNode->GetXFloat(), addon->RootNode->GetYFloat());
}
addon->RootNode->SetPositionFloat(-9999.0f, -9999.0f);
});
_hidingCastBar = true;
}
else if ((forceVisible || !Config.HideDefaultCastbar) && _hidingCastBar)
{
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PreDraw, "_CastBar");
AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_CastBar", 1).Address;
if (addon != null)
{
addon->RootNode->SetPositionFloat(_castBarPos.X, _castBarPos.Y);
}
_hidingCastBar = false;
}
return;
}
private unsafe void UpdateDefaultPulltimer(bool forceVisible = false)
{
if (Config.HideDefaultPulltimer && !_hidingPullTimer)
{
Plugin.AddonLifecycle.RegisterListener(AddonEvent.PreDraw, "ScreenInfo_CountDown", (addonEvent, args) =>
{
AtkUnitBase* addon = (AtkUnitBase*)args.Addon.Address;
if (!_hidingPullTimer)
{
_pullTimerPos = new Vector2(addon->RootNode->GetXFloat(), addon->RootNode->GetYFloat());
}
addon->RootNode->SetPositionFloat(-9999.0f, -9999.0f);
});
AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("ScreenInfo_CountDown", 1).Address;
if (addon != null)
{
_pullTimerPos = new Vector2(addon->RootNode->GetXFloat(), addon->RootNode->GetYFloat());
addon->RootNode->SetPositionFloat(-9999, -9999);
}
_hidingPullTimer = true;
}
else if ((forceVisible || !Config.HideDefaultPulltimer) && _hidingPullTimer)
{
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PreDraw, "ScreenInfo_CountDown");
AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("ScreenInfo_CountDown", 1).Address;
if (addon != null)
{
addon->RootNode->SetPositionFloat(_pullTimerPos.X, _pullTimerPos.Y);
}
_hidingPullTimer = false;
}
return;
}
private static readonly Dictionary<uint, Type> _jobIdToConfigType = new()
{
[JobIDs.PLD] = typeof(PaladinConfig), [JobIDs.WAR] = typeof(WarriorConfig),
[JobIDs.DRK] = typeof(DarkKnightConfig), [JobIDs.GNB] = typeof(GunbreakerConfig),
[JobIDs.WHM] = typeof(WhiteMageConfig), [JobIDs.SCH] = typeof(ScholarConfig),
[JobIDs.AST] = typeof(AstrologianConfig), [JobIDs.SGE] = typeof(SageConfig),
[JobIDs.MNK] = typeof(MonkConfig), [JobIDs.DRG] = typeof(DragoonConfig),
[JobIDs.NIN] = typeof(NinjaConfig), [JobIDs.SAM] = typeof(SamuraiConfig),
[JobIDs.RPR] = typeof(ReaperConfig), [JobIDs.VPR] = typeof(ViperConfig),
[JobIDs.BRD] = typeof(BardConfig), [JobIDs.MCH] = typeof(MachinistConfig),
[JobIDs.DNC] = typeof(DancerConfig), [JobIDs.BLM] = typeof(BlackMageConfig),
[JobIDs.SMN] = typeof(SummonerConfig), [JobIDs.RDM] = typeof(RedMageConfig),
[JobIDs.BLU] = typeof(BlueMageConfig), [JobIDs.PCT] = typeof(PictomancerConfig),
};
private static bool IsCurrentJobHudEnabled()
{
var player = Plugin.ObjectTable.LocalPlayer;
if (player == null) return false;
if (!_jobIdToConfigType.TryGetValue(player.ClassJob.RowId, out var configType))
return false;
var config = ConfigurationManager.Instance?.GetConfigObjectForType(configType) as JobConfig;
return config?.Enabled ?? false;
}
private static IEnumerable<string> GetCurrentJobGaugeAddonNames()
{
var player = Plugin.ObjectTable.LocalPlayer;
if (player == null) yield break;
if (!JobsHelper.JobNames.TryGetValue(player.ClassJob.RowId, out var jobName))
yield break;
for (int i = 0; i < 4; i++)
{
string addonName = $"JobHud{jobName}{i}";
if (_specialCases.TryGetValue(addonName, out var name) && name != null)
addonName = name;
yield return addonName;
}
}
private bool _hidingNameplates = false;
private bool _nameplateVisibilityBeforeHide = true;
private unsafe void UpdateDefaultNameplates(bool forceVisible = false)
{
var nameplatesConfig = ConfigurationManager.Instance?.GetConfigObject<NameplatesGeneralConfig>();
bool shouldHide = !forceVisible && Config.HideDefaultHudWhenReplaced && (nameplatesConfig?.Enabled ?? false);
var addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("NamePlate", 1).Address;
if (addon == null) return;
if (shouldHide)
{
if (!_hidingNameplates)
{
_nameplateVisibilityBeforeHide = addon->IsVisible;
_hidingNameplates = true;
}
addon->IsVisible = false;
}
else
{
if (_hidingNameplates)
{
addon->IsVisible = _nameplateVisibilityBeforeHide;
_hidingNameplates = false;
}
}
}
private static Dictionary<string, string> _specialCases = new()
{
["JobHudPCT0"] = "JobHudRPM0",
["JobHudPCT1"] = "JobHudRPM1",
["JobHudNIN1"] = "JobHudNIN1v70",
["JobHudVPR0"] = "JobHudRDB0",
["JobHudVPR1"] = "JobHudRDB1"
};
private unsafe void UpdateJobGauges(bool forceVisible = false)
{
IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer;
if (player == null) { return; }
string jobName = JobsHelper.JobNames[player.ClassJob.RowId];
int i = 0;
bool stop = false;
do
{
string addonName = $"JobHud{jobName}{i}";
if (_specialCases.TryGetValue(addonName, out string? name) && name != null)
{
addonName = name;
}
AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName(addonName, 1).Address;
if (addon == null)
{
stop = true;
}
else
{
addon->IsVisible = forceVisible || !Config.HideDefaultJobGauges;
}
i++;
} while (!stop);
}
}
internal class StrataLevelComparer<TKey> : IComparer<TKey> where TKey : PluginConfigObject
{
public int Compare(TKey? a, TKey? b)
{
MovablePluginConfigObject? configA = a is MovablePluginConfigObject ? a as MovablePluginConfigObject : null;
MovablePluginConfigObject? configB = b is MovablePluginConfigObject ? b as MovablePluginConfigObject : null;
if (configA == null && configB == null) { return 0; }
if (configA == null && configB != null) { return -1; }
if (configA != null && configB == null) { return 1; }
if (configA!.StrataLevel == configB!.StrataLevel)
{
return configA.ID.CompareTo(configB.ID);
}
if (configA.StrataLevel < configB.StrataLevel)
{
return -1;
}
return 1;
}
}
}