diff --git a/Helpers/ActionBarsManager.cs b/Helpers/ActionBarsManager.cs
index 4cdab47..b5451bc 100644
--- a/Helpers/ActionBarsManager.cs
+++ b/Helpers/ActionBarsManager.cs
@@ -89,9 +89,48 @@ namespace HSUI.Helpers
return p;
}
+ /// Track PvP state so we can restore PvE bars when leaving PvP (game sometimes leaves PvP data in StandardHotbars).
+ private static bool _pvpHotbarsActiveLastFrame;
+ private static bool _clientStatePvPLastFrame;
+ /// After leaving PvP, keep re-applying PvE load for this many frames in case the game overwrites.
+ private static int _restorePvEFramesLeft;
+
+ ///
+ /// Call once per frame (e.g. from Framework Update). When we detect leaving PvP, loads saved PvE hotbars into the live bars
+ /// and keeps re-applying for a short window so PvE actions are not overwritten by stale PvP state.
+ ///
+ public static unsafe void TryRestorePvEHotbarsAfterLeavePvP()
+ {
+ var module = RaptureHotbarModule.Instance();
+ if (module == null || !module->ModuleReady)
+ return;
+
+ bool pvpActive = module->PvPHotbarsActive;
+ bool clientPvP = Plugin.ClientState.IsPvP;
+
+ if (!pvpActive && !clientPvP)
+ {
+ bool justLeftPvP = _pvpHotbarsActiveLastFrame || _clientStatePvPLastFrame;
+ if (justLeftPvP)
+ _restorePvEFramesLeft = 120; // ~2s at 60fps
+
+ if (_restorePvEFramesLeft > 0)
+ {
+ uint classJobId = (uint)(module->ActiveHotbarClassJobId & 0x7F);
+ for (uint barId = 0; barId < 10; barId++)
+ module->LoadSavedHotbar(classJobId, barId);
+ _restorePvEFramesLeft--;
+ }
+ }
+
+ _pvpHotbarsActiveLastFrame = pvpActive;
+ _clientStatePvPLastFrame = clientPvP;
+ }
+
///
/// Reads hotbar slot data from RaptureHotbarModule. Returns up to slotCount slots.
/// hotbarIndex 1-10 maps to StandardHotbars 0-9.
+ /// Always reads from live Hotbars; when leaving PvP, TryRestorePvEHotbarsAfterLeavePvP loads saved PvE into live so we then show PvE.
///
public unsafe List GetSlotData(int hotbarIndex, int slotCount)
{
@@ -100,11 +139,11 @@ namespace HSUI.Helpers
if (module == null || !module->ModuleReady)
return list;
- var hotbars = module->StandardHotbars;
int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
int count = Math.Clamp(slotCount, 1, 12);
-
+ var hotbars = module->StandardHotbars;
ref var bar = ref hotbars[barIdx];
+
for (int i = 0; i < count; i++)
{
var slot = bar.GetHotbarSlot((uint)i);
@@ -116,7 +155,6 @@ namespace HSUI.Helpers
continue;
}
- // GearSet with id 0 is valid (first gearset); the game's IsEmpty (CommandId == 0) would wrongly treat it as empty.
bool isEmpty = slot->IsEmpty && slot->CommandType != RaptureHotbarModule.HotbarSlotType.GearSet;
if (isEmpty)
{
@@ -124,7 +162,6 @@ namespace HSUI.Helpers
continue;
}
- // Use CommandType/CommandId for GearSet so we handle gearset 0 and slots not yet synced to Apparent*.
var slotType = slot->CommandType == RaptureHotbarModule.HotbarSlotType.GearSet
? RaptureHotbarModule.HotbarSlotType.GearSet
: slot->ApparentSlotType;
@@ -132,13 +169,11 @@ namespace HSUI.Helpers
? slot->CommandId
: slot->ApparentActionId;
- // For GearSet slots, refresh IconId from the gearset (e.g. job icon from first equipment slot).
if (slotType == RaptureHotbarModule.HotbarSlotType.GearSet)
slot->LoadIconId();
bool usable = slot->IsSlotUsable(slotType, actionId);
uint iconId = slot->IconId;
- // GearSet 0 or just-dropped: game may not have synced Apparent* so IconId can be 0; resolve for display.
if (slotType == RaptureHotbarModule.HotbarSlotType.GearSet && iconId == 0)
{
int resolved = slot->GetIconIdForSlot(slotType, actionId);
@@ -148,8 +183,6 @@ namespace HSUI.Helpers
(int pct, int secsLeft) = GetSlotCooldown(slot);
(int currentCharges, int maxCharges) = GetSlotCharges(slotType, actionId);
- // For charge-based actions, don't grey out the icon until all charges are spent.
- // Use both the slot's recast-charge count and ActionManager so we catch all cases.
uint apparentCharges = slotType == RaptureHotbarModule.HotbarSlotType.Action ? slot->GetApparentIconRecastCharges() : 0;
if (maxCharges > 1 && (apparentCharges > 0 || currentCharges > 0))
usable = true;
diff --git a/Helpers/ActionChatLinkHelper.cs b/Helpers/ActionChatLinkHelper.cs
index 0ceef2f..0973d60 100644
--- a/Helpers/ActionChatLinkHelper.cs
+++ b/Helpers/ActionChatLinkHelper.cs
@@ -52,50 +52,58 @@ namespace HSUI.Helpers
private void OnFrameworkUpdate(IFramework framework)
{
- if (!_config.ActionChatLinkEnabled) return;
-
- bool shiftHeld = ImGui.IsKeyDown(ImGuiKey.LeftShift) || ImGui.IsKeyDown(ImGuiKey.RightShift);
- bool leftClicked = ImGui.IsMouseClicked(ImGuiMouseButton.Left);
-
- // ActionMenu must be visible
- var addon = Plugin.GameGui.GetAddonByName("ActionMenu", 1);
- if (addon == null || addon.Address == IntPtr.Zero)
+ try
{
- _pendingActionId = 0;
- return;
- }
+ if (!_config.ActionChatLinkEnabled) return;
- // Capture action: 1) Addon list (ActionList or TraitList); 2) HoveredAction only when mouse is over ActionMenu (excludes hotbars)
- if (shiftHeld)
- {
- if (TryGetHoveredActionFromAddon(addon.Address, out uint fromAddon))
- _pendingActionId = fromAddon;
- else if (IsMouseOverActionMenu(addon.Address) && !ActionBarsHitTestHelper.IsMouseOverAnyHSUIHotbar())
+ bool shiftHeld = ImGui.IsKeyDown(ImGuiKey.LeftShift) || ImGui.IsKeyDown(ImGuiKey.RightShift);
+ bool leftClicked = ImGui.IsMouseClicked(ImGuiMouseButton.Left);
+
+ // ActionMenu must be visible
+ var addon = Plugin.GameGui.GetAddonByName("ActionMenu", 1);
+ if (addon == null || addon.Address == IntPtr.Zero)
{
- var ha = Plugin.GameGui.HoveredAction;
- if (ha != null && ha.ActionKind != HoverActionKind.None && ha.ActionID != 0)
- _pendingActionId = ha.ActionID;
+ _pendingActionId = 0;
+ return;
+ }
+
+ // Capture action: 1) Addon list (ActionList or TraitList); 2) HoveredAction only when mouse is over ActionMenu (excludes hotbars)
+ if (shiftHeld)
+ {
+ if (TryGetHoveredActionFromAddon(addon.Address, out uint fromAddon))
+ _pendingActionId = fromAddon;
+ else if (IsMouseOverActionMenu(addon.Address) && !ActionBarsHitTestHelper.IsMouseOverAnyHSUIHotbar())
+ {
+ var ha = Plugin.GameGui.HoveredAction;
+ if (ha != null && ha.ActionKind != HoverActionKind.None && ha.ActionID != 0)
+ _pendingActionId = ha.ActionID;
+ else
+ _pendingActionId = 0;
+ }
else
_pendingActionId = 0;
}
else
_pendingActionId = 0;
- }
- else
+
+ // On shift+click, use captured action or HoveredAction as fallback
+ if (!shiftHeld || !leftClicked) return;
+
+ if (_pendingActionId == 0) return;
+ uint idToUse = _pendingActionId;
+
+ string? actionName = GetActionName(idToUse);
+ if (string.IsNullOrWhiteSpace(actionName)) return;
+
+ string text = $"You should check out {actionName}";
+ InsertOrCopyToChat(text);
_pendingActionId = 0;
-
- // On shift+click, use captured action or HoveredAction as fallback
- if (!shiftHeld || !leftClicked) return;
-
- if (_pendingActionId == 0) return;
- uint idToUse = _pendingActionId;
-
- string? actionName = GetActionName(idToUse);
- if (string.IsNullOrWhiteSpace(actionName)) return;
-
- string text = $"You should check out {actionName}";
- InsertOrCopyToChat(text);
- _pendingActionId = 0;
+ }
+ catch (Exception ex)
+ {
+ _pendingActionId = 0;
+ Plugin.Logger.Warning($"[ActionChatLink] OnFrameworkUpdate: {ex.Message}");
+ }
}
/// Read action ID from AddonActionMenu. Tries ActionList and TraitList, HoveredItemIndex and HeldItemIndex.
@@ -104,19 +112,26 @@ namespace HSUI.Helpers
actionId = 0;
if (addonAddress == IntPtr.Zero) return false;
- var addon = (AddonActionMenu*)addonAddress;
- byte* basePtr = (byte*)addonAddress;
-
- foreach (var list in new[] { addon->ActionList, addon->TraitList })
+ try
{
- if (list == null) continue;
- foreach (int idx in new[] { list->HoveredItemIndex, list->HeldItemIndex })
+ var addon = (AddonActionMenu*)addonAddress;
+ byte* basePtr = (byte*)addonAddress;
+
+ foreach (var list in new[] { addon->ActionList, addon->TraitList })
{
- if (idx < 0 || idx >= 80) continue;
- actionId = *(uint*)(basePtr + 0x338 + idx * 0x38 + 0x14);
- if (actionId != 0) return true;
+ if (list == null) continue;
+ foreach (int idx in new[] { list->HoveredItemIndex, list->HeldItemIndex })
+ {
+ if (idx < 0 || idx >= 80) continue;
+ actionId = *(uint*)(basePtr + 0x338 + idx * 0x38 + 0x14);
+ if (actionId != 0) return true;
+ }
}
}
+ catch (Exception ex)
+ {
+ Plugin.Logger.Verbose($"[ActionChatLink] TryGetHoveredActionFromAddon: {ex.Message}");
+ }
return false;
}
@@ -124,13 +139,21 @@ namespace HSUI.Helpers
private static unsafe bool IsMouseOverActionMenu(IntPtr addonAddress)
{
if (addonAddress == IntPtr.Zero) return false;
- var addon = (AtkUnitBase*)addonAddress;
- var root = addon->RootNode;
- if (root == null || !addon->IsVisible) return false;
+ try
+ {
+ var addon = (AtkUnitBase*)addonAddress;
+ var root = addon->RootNode;
+ if (root == null || !addon->IsVisible) return false;
- var mp = ImGui.GetMousePos();
- float x = root->ScreenX, y = root->ScreenY, w = root->Width, h = root->Height;
- return mp.X >= x && mp.X < x + w && mp.Y >= y && mp.Y < y + h;
+ var mp = ImGui.GetMousePos();
+ float x = root->ScreenX, y = root->ScreenY, w = root->Width, h = root->Height;
+ return mp.X >= x && mp.X < x + w && mp.Y >= y && mp.Y < y + h;
+ }
+ catch (Exception ex)
+ {
+ Plugin.Logger.Verbose($"[ActionChatLink] IsMouseOverActionMenu: {ex.Message}");
+ return false;
+ }
}
private static string? GetActionName(uint actionId)
diff --git a/Helpers/InputsHelper.cs b/Helpers/InputsHelper.cs
index e562d0a..d55c635 100644
--- a/Helpers/InputsHelper.cs
+++ b/Helpers/InputsHelper.cs
@@ -470,18 +470,28 @@ namespace HSUI.Helpers
public void OnFrameworkUpdate(IFramework framework)
{
- // Keep WndProc hooked when: proxy mode (for mouseover) OR we need to block game drag
- // release (so dropping on HSUI action bar doesn't execute the ability).
- bool needHook = IsProxyEnabled || ShouldBlockGameDragRelease();
- if (needHook && _wndProcPtr == IntPtr.Zero)
+ try
{
- HookWndProc();
- // Only log when we actually installed (HookWndProc can return early during init delay)
- if (_wndProcPtr != IntPtr.Zero && IsActionBarDragDropDebugEnabled())
- Plugin.Logger.Information("[HSUI DragDrop DBG] WndProc hook INSTALLED (needHook=true for drag-block)");
+ // When leaving PvP, force load PvE hotbars so HSUI bars don't keep showing PvP actions.
+ ActionBarsManager.TryRestorePvEHotbarsAfterLeavePvP();
+
+ // Keep WndProc hooked when: proxy mode (for mouseover) OR we need to block game drag
+ // release (so dropping on HSUI action bar doesn't execute the ability).
+ bool needHook = IsProxyEnabled || ShouldBlockGameDragRelease();
+ if (needHook && _wndProcPtr == IntPtr.Zero)
+ {
+ HookWndProc();
+ // Only log when we actually installed (HookWndProc can return early during init delay)
+ if (_wndProcPtr != IntPtr.Zero && IsActionBarDragDropDebugEnabled())
+ Plugin.Logger.Information("[HSUI DragDrop DBG] WndProc hook INSTALLED (needHook=true for drag-block)");
+ }
+ else if (!needHook && _wndProcPtr != IntPtr.Zero)
+ RestoreWndProc();
+ }
+ catch (Exception ex)
+ {
+ Plugin.Logger.Warning($"[HSUI InputsHelper] OnFrameworkUpdate: {ex.Message}");
}
- else if (!needHook && _wndProcPtr != IntPtr.Zero)
- RestoreWndProc();
}
private static bool ShouldBlockGameDragRelease()
diff --git a/Helpers/MpTickHelper.cs b/Helpers/MpTickHelper.cs
index d3e6943..b4a3492 100644
--- a/Helpers/MpTickHelper.cs
+++ b/Helpers/MpTickHelper.cs
@@ -43,35 +43,34 @@ namespace HSUI.Helpers
private void FrameworkOnOnUpdateEvent(IFramework framework)
{
- var player = Plugin.ObjectTable.LocalPlayer;
- if (player is null)
+ try
{
- return;
- }
+ var player = Plugin.ObjectTable.LocalPlayer;
+ if (player is null)
+ return;
- var now = ImGui.GetTime();
- if (now - LastUpdate < PollingRate)
+ var now = ImGui.GetTime();
+ if (now - LastUpdate < PollingRate)
+ return;
+
+ LastUpdate = now;
+
+ var mp = player.CurrentMp;
+
+ // account for lucid dreaming screwing up mp calculations
+ var lucidDreamingActive = Utils.StatusListForBattleChara(player).Any(e => e.StatusId == 1204);
+
+ if (!lucidDreamingActive && _lastMpValue < mp)
+ LastTickTime = now;
+ else if (LastTickTime + ServerTickRate <= now)
+ LastTickTime += ServerTickRate;
+
+ _lastMpValue = (int)mp;
+ }
+ catch (Exception ex)
{
- return;
+ Plugin.Logger.Verbose($"[HSUI MpTickHelper] FrameworkOnOnUpdateEvent: {ex.Message}");
}
-
- LastUpdate = now;
-
- var mp = player.CurrentMp;
-
- // account for lucid dreaming screwing up mp calculations
- var lucidDreamingActive = Utils.StatusListForBattleChara(player).Any(e => e.StatusId == 1204);
-
- if (!lucidDreamingActive && _lastMpValue < mp)
- {
- LastTickTime = now;
- }
- else if (LastTickTime + ServerTickRate <= now)
- {
- LastTickTime += ServerTickRate;
- }
-
- _lastMpValue = (int)mp;
}
protected virtual void Dispose(bool disposing)
diff --git a/Helpers/TooltipsHelper.cs b/Helpers/TooltipsHelper.cs
index 56fa8f4..c5992e2 100644
--- a/Helpers/TooltipsHelper.cs
+++ b/Helpers/TooltipsHelper.cs
@@ -13,6 +13,14 @@ using System.Text;
namespace HSUI.Helpers
{
+ /// When showing an ID in the tooltip title, use Action for action IDs or Status for status effect IDs.
+ public enum TooltipIdKind
+ {
+ None = 0,
+ Action = 1,
+ Status = 2,
+ }
+
public class TooltipsHelper : IDisposable
{
#region Singleton
@@ -65,12 +73,12 @@ namespace HSUI.Helpers
private const float IconSize = 24f;
- public void ShowTooltipOnCursor(string text, string? title = null, uint id = 0, string name = "", uint? iconId = null)
+ public void ShowTooltipOnCursor(string text, string? title = null, uint id = 0, string name = "", uint? iconId = null, TooltipIdKind idKind = TooltipIdKind.None)
{
- ShowTooltip(text, ImGui.GetMousePos(), title, id, name, iconId);
+ ShowTooltip(text, ImGui.GetMousePos(), title, id, name, iconId, idKind);
}
- public void ShowTooltip(string text, Vector2 position, string? title = null, uint id = 0, string name = "", uint? iconId = null)
+ public void ShowTooltip(string text, Vector2 position, string? title = null, uint id = 0, string name = "", uint? iconId = null, TooltipIdKind idKind = TooltipIdKind.None)
{
if (text == null)
{
@@ -99,7 +107,11 @@ namespace HSUI.Helpers
_currentTooltipTitle += $" ({name})";
}
- if (_config.ShowStatusIDs)
+ bool showId = id != 0 && (
+ (idKind == TooltipIdKind.Action && _config.ShowActionIDs) ||
+ (idKind == TooltipIdKind.Status && _config.ShowStatusIDs) ||
+ (idKind == TooltipIdKind.None && _config.ShowStatusIDs));
+ if (showId)
{
_currentTooltipTitle += " (ID: " + id + ")";
}
@@ -407,6 +419,10 @@ namespace HSUI.Helpers
[Order(7)]
public bool ShowStatusIDs = false;
+ [Checkbox("Show Action ID", help = "Show action ID in hotbar and action tooltips.")]
+ [Order(8)]
+ public bool ShowActionIDs = false;
+
[Checkbox("Show Source Name")]
[Order(10)]
public bool ShowSourceName = false;
diff --git a/Interface/GeneralElements/ActionBarsHud.cs b/Interface/GeneralElements/ActionBarsHud.cs
index 766b20b..1c08def 100644
--- a/Interface/GeneralElements/ActionBarsHud.cs
+++ b/Interface/GeneralElements/ActionBarsHud.cs
@@ -370,7 +370,7 @@ namespace HSUI.Interface.GeneralElements
if (!string.IsNullOrEmpty(title) || !string.IsNullOrEmpty(text))
{
string body = string.IsNullOrEmpty(text) ? title : text;
- TooltipsHelper.Instance.ShowTooltipOnCursor(body, title, slot.ActionId, "", slot.IconId > 0 ? slot.IconId : null);
+ TooltipsHelper.Instance.ShowTooltipOnCursor(body, title, slot.ActionId, "", slot.IconId > 0 ? slot.IconId : null, Helpers.TooltipIdKind.Action);
if (IsTooltipDebugEnabled())
Plugin.Logger.Information($"[HSUI Tooltip DBG] ActionBar tooltip (main overlay): slot={i} title='{title}'");
}
@@ -429,7 +429,7 @@ namespace HSUI.Interface.GeneralElements
if (!string.IsNullOrEmpty(title) || !string.IsNullOrEmpty(text))
{
string body = string.IsNullOrEmpty(text) ? title : text;
- TooltipsHelper.Instance.ShowTooltipOnCursor(body, title, slot.ActionId, "", slot.IconId > 0 ? slot.IconId : null);
+ TooltipsHelper.Instance.ShowTooltipOnCursor(body, title, slot.ActionId, "", slot.IconId > 0 ? slot.IconId : null, Helpers.TooltipIdKind.Action);
if (IsTooltipDebugEnabled())
Plugin.Logger.Information($"[HSUI Tooltip DBG] ActionBar tooltip (overlay): slot={i} title='{title}'");
}
diff --git a/Interface/Party/PartyFramesCooldownListHud.cs b/Interface/Party/PartyFramesCooldownListHud.cs
index 819daee..bd075ef 100644
--- a/Interface/Party/PartyFramesCooldownListHud.cs
+++ b/Interface/Party/PartyFramesCooldownListHud.cs
@@ -289,7 +289,8 @@ namespace HSUI.Interface.Party
hoveringCooldown.Data.Name,
hoveringCooldown.Data.ActionId,
"",
- iconId
+ iconId,
+ HSUI.Helpers.TooltipIdKind.Action
);
}
}
diff --git a/Interface/PartyCooldowns/PartyCooldownsHud.cs b/Interface/PartyCooldowns/PartyCooldownsHud.cs
index 5118cdf..cb9cb5f 100644
--- a/Interface/PartyCooldowns/PartyCooldownsHud.cs
+++ b/Interface/PartyCooldowns/PartyCooldownsHud.cs
@@ -310,7 +310,8 @@ namespace HSUI.Interface.PartyCooldowns
cooldown.Data.Name,
cooldown.Data.ActionId,
"",
- iconId
+ iconId,
+ HSUI.Helpers.TooltipIdKind.Action
);
}
diff --git a/Interface/StatusEffects/StatusEffectsListHud.cs b/Interface/StatusEffects/StatusEffectsListHud.cs
index fc8acb6..b5a8683 100644
--- a/Interface/StatusEffects/StatusEffectsListHud.cs
+++ b/Interface/StatusEffects/StatusEffectsListHud.cs
@@ -474,7 +474,8 @@ namespace HSUI.Interface.StatusEffects
EncryptedStringsHelper.GetString(data.Data.Name.ToString()),
data.Status.StatusId,
GetStatusActorName(data.Status),
- iconId
+ iconId,
+ HSUI.Helpers.TooltipIdKind.Status
);
}