Initial release: HSUI v1.0.0.0 - HUD replacement with configurable hotbars
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,506 @@
|
||||
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;
|
||||
|
||||
// Only restore defaults when already on framework thread. Skip RunOnFrameworkThread
|
||||
// during unload—it can deadlock. Restore is best-effort; game state may be torn down.
|
||||
if (Plugin.Framework.IsInFrameworkUpdateThread && Plugin.ObjectTable.LocalPlayer != null)
|
||||
{
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user