using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using HSUI.Config; using HSUI.Enums; 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(); UpdateDefaultCastBar(); 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(); UpdateDefaultCastBar(); 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 AddElements(params ElementKind[] elements) { foreach (var e in elements) hashesToHide.Add((uint)e); } // Use ElementKind hashes (HUD Layout config) — the proper way per DelvUI. Source: HUDManager ElementKind.cs var hotbarsConfig = ConfigurationManager.Instance?.GetConfigObject(); if (hotbarsConfig?.Enabled == true && AnyHotbarEnabled()) { AddElements(ElementKind.Hotbar1, ElementKind.Hotbar2, ElementKind.Hotbar3, ElementKind.Hotbar4, ElementKind.Hotbar5, ElementKind.Hotbar6, ElementKind.Hotbar7, ElementKind.Hotbar8, ElementKind.Hotbar9, ElementKind.Hotbar10, ElementKind.CrossHotbar); } var playerUnitFrame = ConfigurationManager.Instance?.GetConfigObject(); if (playerUnitFrame?.Enabled == true) AddElements(ElementKind.ParameterBar); var targetUnitFrame = ConfigurationManager.Instance?.GetConfigObject(); if (targetUnitFrame?.Enabled == true) AddElements(ElementKind.TargetBar, ElementKind.TargetInfoHp, ElementKind.TargetInfoProgressBar, ElementKind.TargetInfoStatus); var focusUnitFrame = ConfigurationManager.Instance?.GetConfigObject(); if (focusUnitFrame?.Enabled == true) AddElements(ElementKind.FocusTargetBar); var playerCastbar = ConfigurationManager.Instance?.GetConfigObject(); if (playerCastbar?.Enabled == true) AddElements(ElementKind.ProgressBar); var expBar = ConfigurationManager.Instance?.GetConfigObject(); if (expBar?.Enabled == true) AddElements(ElementKind.ExperienceBar); var limitBreak = ConfigurationManager.Instance?.GetConfigObject(); if (limitBreak?.Enabled == true) AddElements(ElementKind.LimitGauge); var playerBuffs = ConfigurationManager.Instance?.GetConfigObject(); var playerDebuffs = ConfigurationManager.Instance?.GetConfigObject(); if (playerBuffs?.Enabled == true || playerDebuffs?.Enabled == true) AddElements(ElementKind.StatusEffects, ElementKind.StatusInfoEnhancements, ElementKind.StatusInfoEnfeeblements, ElementKind.StatusInfoOther, ElementKind.StatusInfoConditionalEnhancements); var partyFrames = ConfigurationManager.Instance?.GetConfigObject(); if (partyFrames?.Enabled == true) AddElements(ElementKind.PartyList); var enemyList = ConfigurationManager.Instance?.GetConfigObject(); if (enemyList?.Enabled == true) AddElements(ElementKind.EnemyList); // NamePlate is not in HUD Layout config; use UpdateDefaultNameplates (IsVisible) for that if (IsCurrentJobHudEnabled()) { var player = Plugin.ObjectTable.LocalPlayer; if (player != null) { foreach (var ek in ElementKindHelper.GetJobGaugeElementKinds(player.ClassJob.RowId)) hashesToHide.Add((uint)ek); } } 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) { // Hide when explicit config says so, or when HSUI is replacing it (HideDefaultHudWhenReplaced + player castbar enabled) bool playerCastbarEnabled = ConfigurationManager.Instance?.GetConfigObject()?.Enabled ?? false; bool shouldHide = Config.HideDefaultCastbar || (Config.HideDefaultHudWhenReplaced && playerCastbarEnabled); if (shouldHide && !_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 || !shouldHide) && _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; } } }