Files
HSUI/Interface/HudHelper.cs
T

668 lines
27 KiB
C#

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<HUDOptionsConfig>();
private bool _firstUpdate = true;
private Vector2 _castBarPos = Vector2.Zero;
private bool _hidingCastBar = false;
private Vector2 _pullTimerPos = Vector2.Zero;
private bool _hidingPullTimer = false;
private bool _hidingDutyListOffScreen = false;
private bool _toDoListVisibleBeforeHide = true;
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);
UpdateDefaultDutyList(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();
UpdateDefaultDutyList();
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)
{
// When this element's context menu is open, ignore visibility so the element stays visible
if (ContextMenuVisibilityHelper.IsContextMenuOpenFor(element))
return false;
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<uint>(), false);
return;
}
var hashesToHide = new List<uint>();
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<HotbarsConfig>();
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<PlayerUnitFrameConfig>();
if (playerUnitFrame?.Enabled == true)
AddElements(ElementKind.ParameterBar);
var targetUnitFrame = ConfigurationManager.Instance?.GetConfigObject<TargetUnitFrameConfig>();
if (targetUnitFrame?.Enabled == true)
AddElements(ElementKind.TargetBar, ElementKind.TargetInfoHp, ElementKind.TargetInfoProgressBar, ElementKind.TargetInfoStatus);
var focusUnitFrame = ConfigurationManager.Instance?.GetConfigObject<FocusTargetUnitFrameConfig>();
if (focusUnitFrame?.Enabled == true)
AddElements(ElementKind.FocusTargetBar);
var playerCastbar = ConfigurationManager.Instance?.GetConfigObject<PlayerCastbarConfig>();
if (playerCastbar?.Enabled == true)
AddElements(ElementKind.ProgressBar);
var expBar = ConfigurationManager.Instance?.GetConfigObject<ExperienceBarConfig>();
if (expBar?.Enabled == true)
AddElements(ElementKind.ExperienceBar);
var limitBreak = ConfigurationManager.Instance?.GetConfigObject<LimitBreakConfig>();
if (limitBreak?.Enabled == true)
AddElements(ElementKind.LimitGauge);
var playerBuffs = ConfigurationManager.Instance?.GetConfigObject<PlayerBuffsListConfig>();
var playerDebuffs = ConfigurationManager.Instance?.GetConfigObject<PlayerDebuffsListConfig>();
if (playerBuffs?.Enabled == true || playerDebuffs?.Enabled == true)
AddElements(ElementKind.StatusEffects, ElementKind.StatusInfoEnhancements, ElementKind.StatusInfoEnfeeblements, ElementKind.StatusInfoOther, ElementKind.StatusInfoConditionalEnhancements);
var partyFrames = ConfigurationManager.Instance?.GetConfigObject<PartyFramesConfig>();
if (partyFrames?.Enabled == true)
AddElements(ElementKind.PartyList);
var alliance1 = ConfigurationManager.Instance?.GetConfigObject<AllianceFrames1Config>();
var alliance2 = ConfigurationManager.Instance?.GetConfigObject<AllianceFrames2Config>();
if ((alliance1?.Enabled == true || alliance2?.Enabled == true))
AddElements(ElementKind.AllianceList1, ElementKind.AllianceList2);
var enemyList = ConfigurationManager.Instance?.GetConfigObject<EnemyListConfig>();
if (enemyList?.Enabled == true)
AddElements(ElementKind.EnemyList);
var dutyListScenario = ConfigurationManager.Instance?.GetConfigObject<DutyListScenarioConfig>();
if (dutyListScenario?.Enabled == true)
{
// Only hide Scenario Tree via layout. Keep ToDoList in layout so it stays created/updated; we move it off-screen in UpdateDefaultDutyList so we can read objective text.
AddElements(ElementKind.ScenarioTree);
}
var mainMenu = ConfigurationManager.Instance?.GetConfigObject<MainMenuConfig>();
// Disabled while troubleshooting: keep game's Main Menu visible so we can compare behavior.
// if (mainMenu?.Enabled == true)
// AddElements(ElementKind.MainMenu);
// 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<Hotbar1BarConfig>(),
cfg.GetConfigObject<Hotbar2BarConfig>(),
cfg.GetConfigObject<Hotbar3BarConfig>(),
cfg.GetConfigObject<Hotbar4BarConfig>(),
cfg.GetConfigObject<Hotbar5BarConfig>(),
cfg.GetConfigObject<Hotbar6BarConfig>(),
cfg.GetConfigObject<Hotbar7BarConfig>(),
cfg.GetConfigObject<Hotbar8BarConfig>(),
cfg.GetConfigObject<Hotbar9BarConfig>(),
cfg.GetConfigObject<Hotbar10BarConfig>()
};
foreach (var bar in bars)
{
if (bar?.Enabled == true) return true;
}
return false;
}
/// <summary>Visibility (ByteValue2) of game HUD elements before we hid them. Restored on disable/unload.</summary>
private readonly Dictionary<uint, byte> _gameHudVisibilityBeforeHide = new();
/// <summary>When we force-show ToDoList for off-screen text reading, save its ByteValue2 here to restore on disable.</summary>
private readonly Dictionary<uint, byte> _gameHudForceShowBeforeRestore = 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();
foreach (ref var entry in entries)
{
if (!_gameHudForceShowBeforeRestore.TryGetValue(entry.AddonNameHash, out byte saved))
continue;
if (entry.ByteValue2 == saved)
continue;
entry.ByteValue2 = saved;
hasChanges = true;
}
_gameHudForceShowBeforeRestore.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;
}
// When Duty List/Scenario is enabled, force-show the Duty List (ToDoList) so the addon is created and we can move it off-screen for text reading
var dutyListScenario = ConfigurationManager.Instance?.GetConfigObject<DutyListScenarioConfig>();
if (dutyListScenario?.Enabled == true)
{
uint toDoListHash = (uint)ElementKind.ToDoList;
foreach (ref var entry in entries)
{
if (entry.AddonNameHash != toDoListHash)
continue;
if (!_gameHudForceShowBeforeRestore.ContainsKey(entry.AddonNameHash))
_gameHudForceShowBeforeRestore[entry.AddonNameHash] = entry.ByteValue2;
if (entry.ByteValue2 != 0x0)
continue;
entry.ByteValue2 = 0x1;
hasChanges = true;
break;
}
}
else
{
uint toDoListHash = (uint)ElementKind.ToDoList;
if (_gameHudForceShowBeforeRestore.TryGetValue(toDoListHash, out byte saved))
{
foreach (ref var entry in entries)
{
if (entry.AddonNameHash != toDoListHash)
continue;
if (entry.ByteValue2 != saved)
{
entry.ByteValue2 = saved;
hasChanges = true;
}
_gameHudForceShowBeforeRestore.Remove(toDoListHash);
break;
}
}
}
}
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<PlayerCastbarConfig>()?.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;
}
/// <summary>When Duty List/Scenario is enabled, hide the game's Duty List addon with IsVisible=false so it stays at layout position and remains populated for objective text; restore on disable.</summary>
private unsafe void UpdateDefaultDutyList(bool forceRestore = false)
{
var dutyListScenario = ConfigurationManager.Instance?.GetConfigObject<DutyListScenarioConfig>();
bool shouldHide = !forceRestore && (dutyListScenario?.Enabled ?? false);
if (shouldHide && !_hidingDutyListOffScreen)
{
Plugin.AddonLifecycle.RegisterListener(AddonEvent.PreDraw, "_ToDoList", (addonEvent, args) =>
{
AtkUnitBase* addon = (AtkUnitBase*)args.Addon.Address;
if (addon == null) return;
addon->IsVisible = false;
});
var addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_ToDoList", 1).Address;
if (addon == null) addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_ToDoList", 0).Address;
if (addon != null)
{
_toDoListVisibleBeforeHide = addon->IsVisible;
addon->IsVisible = false;
}
_hidingDutyListOffScreen = true;
}
else if ((forceRestore || !shouldHide) && _hidingDutyListOffScreen)
{
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PreDraw, "_ToDoList");
var addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_ToDoList", 1).Address;
if (addon == null) addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_ToDoList", 0).Address;
if (addon != null)
addon->IsVisible = _toDoListVisibleBeforeHide;
_toDoListVisibleBeforeHide = true;
_hidingDutyListOffScreen = false;
}
}
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;
}
}
}