PvP hotbar fix: load saved PvE on zone leave then use live bars; Show Action ID in tooltips
- On leaving PvP, LoadSavedHotbar for all 10 bars (via TryRestorePvEHotbarsAfterLeavePvP in Framework update) and re-apply for ~2s so live Hotbars show PvE - GetSlotData always reads from live StandardHotbars so combo updates (e.g. Pictomancer) and icons work normally - Misc: Show Action ID option in Misc -> Tooltips; hotbar/party cooldown tooltips pass TooltipIdKind for Action vs Status IDs Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -89,9 +89,48 @@ namespace HSUI.Helpers
|
||||
return p;
|
||||
}
|
||||
|
||||
/// <summary>Track PvP state so we can restore PvE bars when leaving PvP (game sometimes leaves PvP data in StandardHotbars).</summary>
|
||||
private static bool _pvpHotbarsActiveLastFrame;
|
||||
private static bool _clientStatePvPLastFrame;
|
||||
/// <summary>After leaving PvP, keep re-applying PvE load for this many frames in case the game overwrites.</summary>
|
||||
private static int _restorePvEFramesLeft;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public unsafe List<SlotInfo> 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;
|
||||
|
||||
@@ -51,6 +51,8 @@ namespace HSUI.Helpers
|
||||
_config = ConfigurationManager.Instance.GetConfigObject<WorldObjectTooltipConfig>();
|
||||
|
||||
private void OnFrameworkUpdate(IFramework framework)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_config.ActionChatLinkEnabled) return;
|
||||
|
||||
@@ -97,6 +99,12 @@ namespace HSUI.Helpers
|
||||
InsertOrCopyToChat(text);
|
||||
_pendingActionId = 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_pendingActionId = 0;
|
||||
Plugin.Logger.Warning($"[ActionChatLink] OnFrameworkUpdate: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Read action ID from AddonActionMenu. Tries ActionList and TraitList, HoveredItemIndex and HeldItemIndex.</summary>
|
||||
private static unsafe bool TryGetHoveredActionFromAddon(IntPtr addonAddress, out uint actionId)
|
||||
@@ -104,6 +112,8 @@ namespace HSUI.Helpers
|
||||
actionId = 0;
|
||||
if (addonAddress == IntPtr.Zero) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var addon = (AddonActionMenu*)addonAddress;
|
||||
byte* basePtr = (byte*)addonAddress;
|
||||
|
||||
@@ -117,6 +127,11 @@ namespace HSUI.Helpers
|
||||
if (actionId != 0) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Verbose($"[ActionChatLink] TryGetHoveredActionFromAddon: {ex.Message}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -124,6 +139,8 @@ namespace HSUI.Helpers
|
||||
private static unsafe bool IsMouseOverActionMenu(IntPtr addonAddress)
|
||||
{
|
||||
if (addonAddress == IntPtr.Zero) return false;
|
||||
try
|
||||
{
|
||||
var addon = (AtkUnitBase*)addonAddress;
|
||||
var root = addon->RootNode;
|
||||
if (root == null || !addon->IsVisible) return false;
|
||||
@@ -132,6 +149,12 @@ namespace HSUI.Helpers
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -470,6 +470,11 @@ namespace HSUI.Helpers
|
||||
|
||||
public void OnFrameworkUpdate(IFramework framework)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 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();
|
||||
@@ -483,6 +488,11 @@ namespace HSUI.Helpers
|
||||
else if (!needHook && _wndProcPtr != IntPtr.Zero)
|
||||
RestoreWndProc();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Warning($"[HSUI InputsHelper] OnFrameworkUpdate: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldBlockGameDragRelease()
|
||||
{
|
||||
|
||||
@@ -42,18 +42,16 @@ namespace HSUI.Helpers
|
||||
public double LastTick => LastTickTime;
|
||||
|
||||
private void FrameworkOnOnUpdateEvent(IFramework framework)
|
||||
{
|
||||
try
|
||||
{
|
||||
var player = Plugin.ObjectTable.LocalPlayer;
|
||||
if (player is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = ImGui.GetTime();
|
||||
if (now - LastUpdate < PollingRate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LastUpdate = now;
|
||||
|
||||
@@ -63,16 +61,17 @@ namespace HSUI.Helpers
|
||||
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)
|
||||
{
|
||||
Plugin.Logger.Verbose($"[HSUI MpTickHelper] FrameworkOnOnUpdateEvent: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
|
||||
@@ -13,6 +13,14 @@ using System.Text;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
/// <summary>When showing an ID in the tooltip title, use Action for action IDs or Status for status effect IDs.</summary>
|
||||
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;
|
||||
|
||||
@@ -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}'");
|
||||
}
|
||||
|
||||
@@ -289,7 +289,8 @@ namespace HSUI.Interface.Party
|
||||
hoveringCooldown.Data.Name,
|
||||
hoveringCooldown.Data.ActionId,
|
||||
"",
|
||||
iconId
|
||||
iconId,
|
||||
HSUI.Helpers.TooltipIdKind.Action
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +310,8 @@ namespace HSUI.Interface.PartyCooldowns
|
||||
cooldown.Data.Name,
|
||||
cooldown.Data.ActionId,
|
||||
"",
|
||||
iconId
|
||||
iconId,
|
||||
HSUI.Helpers.TooltipIdKind.Action
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user