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 ); }