95c42af5b8
- 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>
526 lines
20 KiB
C#
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;
|
|
}
|
|
}
|
|
} |