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(); 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(), 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(), false); return; } var hashesToHide = new List(); 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(); if (hotbarsConfig?.Enabled == true) { AddHashes("_ActionBar", "_ActionBar01", "_ActionBar02", "_ActionBar03", "_ActionBar04", "_ActionBar05", "_ActionBar06", "_ActionBar07", "_ActionBar08", "_ActionBar09", "_ActionCross"); } var playerUnitFrame = ConfigurationManager.Instance?.GetConfigObject(); if (playerUnitFrame?.Enabled == true) AddHashes("_ParameterWidget"); var targetUnitFrame = ConfigurationManager.Instance?.GetConfigObject(); if (targetUnitFrame?.Enabled == true) AddHashes("_TargetInfo", "_TargetInfoMainTarget", "_TargetInfoCastBar", "_TargetInfoBuffDebuff"); var focusUnitFrame = ConfigurationManager.Instance?.GetConfigObject(); if (focusUnitFrame?.Enabled == true) AddHashes("_FocusTargetInfo"); var playerCastbar = ConfigurationManager.Instance?.GetConfigObject(); if (playerCastbar?.Enabled == true) AddHashes("_CastBar"); var expBar = ConfigurationManager.Instance?.GetConfigObject(); if (expBar?.Enabled == true) AddHashes("_Exp"); var limitBreak = ConfigurationManager.Instance?.GetConfigObject(); if (limitBreak?.Enabled == true) AddHashes("_LimitBreak"); var playerBuffs = ConfigurationManager.Instance?.GetConfigObject(); var playerDebuffs = ConfigurationManager.Instance?.GetConfigObject(); if (playerBuffs?.Enabled == true || playerDebuffs?.Enabled == true) AddHashes("_Status", "_StatusCustom0", "_StatusCustom1", "_StatusCustom2", "_StatusCustom3"); var partyFrames = ConfigurationManager.Instance?.GetConfigObject(); if (partyFrames?.Enabled == true) AddHashes("_PartyList"); var enemyList = ConfigurationManager.Instance?.GetConfigObject(); 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(); if (nameplatesConfig?.Enabled == true) AddHashes("NamePlate"); if (IsCurrentJobHudEnabled()) { foreach (var name in GetCurrentJobGaugeAddonNames()) AddHashes(name); } SetGameHudElementsHidden(hashesToHide.ToArray(), false); } private static bool AnyHotbarEnabled() { var cfg = ConfigurationManager.Instance; if (cfg == null) return true; var bars = new HotbarBarConfig?[] { cfg.GetConfigObject(), cfg.GetConfigObject(), cfg.GetConfigObject(), cfg.GetConfigObject(), cfg.GetConfigObject(), cfg.GetConfigObject(), cfg.GetConfigObject(), cfg.GetConfigObject(), cfg.GetConfigObject(), cfg.GetConfigObject() }; foreach (var bar in bars) { if (bar?.Enabled == true) return true; } return false; } /// Visibility (ByteValue2) of game HUD elements before we hid them. Restored on disable/unload. private readonly Dictionary _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(hashesToHide); Span 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 _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 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(); 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 _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 : IComparer 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; } } }