bda3762ac8
Co-authored-by: Cursor <cursoragent@cursor.com>
517 lines
25 KiB
C#
517 lines
25 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Runtime.CompilerServices;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||
using KamiToolKit.Controllers;
|
||
using static FFXIVClientStructs.FFXIV.Client.Game.ActionManager;
|
||
|
||
namespace HSUI.Helpers
|
||
{
|
||
public sealed class ActionBarsManager : IDisposable
|
||
{
|
||
public static ActionBarsManager Instance { get; private set; } = null!;
|
||
|
||
private AddonController? _addonController;
|
||
|
||
private ActionBarsManager()
|
||
{
|
||
_addonController = new AddonController("_ActionBar");
|
||
_addonController.Enable();
|
||
}
|
||
|
||
public static void Initialize()
|
||
{
|
||
Instance = new ActionBarsManager();
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
_addonController?.Disable();
|
||
_addonController = null;
|
||
Instance = null!;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Slot data for ImGui drawing. Updated each call from game state.
|
||
/// </summary>
|
||
public readonly struct SlotInfo
|
||
{
|
||
public uint IconId { get; }
|
||
public bool IsEmpty { get; }
|
||
public bool IsUsable { get; }
|
||
public int CooldownPercent { get; }
|
||
public int CooldownSecondsLeft { get; }
|
||
public uint ActionId { get; }
|
||
public RaptureHotbarModule.HotbarSlotType SlotType { get; }
|
||
/// <summary>Keybind hint from game (user's keybind settings). Empty if unavailable.</summary>
|
||
public string KeybindHint { get; }
|
||
|
||
public SlotInfo(uint iconId, bool isEmpty, bool usable, int cooldownPct, int cooldownSecs, uint actionId = 0, RaptureHotbarModule.HotbarSlotType slotType = 0, string keybindHint = "")
|
||
{
|
||
IconId = iconId;
|
||
IsEmpty = isEmpty;
|
||
IsUsable = usable;
|
||
CooldownPercent = cooldownPct;
|
||
CooldownSecondsLeft = cooldownSecs;
|
||
ActionId = actionId;
|
||
SlotType = slotType;
|
||
KeybindHint = keybindHint ?? "";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Reads keybind hint from HotbarSlot. Uses _keybindHint (slot display) then _popUpKeybindHint (trimmed) as fallback.
|
||
/// Offsets match RaptureHotbarModule.HotbarSlot: _keybindHint 0xA8, _popUpKeybindHint 0x88.
|
||
/// </summary>
|
||
private static unsafe string ReadKeybindHintFromSlot(RaptureHotbarModule.HotbarSlot* slot)
|
||
{
|
||
if (slot == null) return "";
|
||
var sb = (byte*)slot;
|
||
string? h = Marshal.PtrToStringUTF8((IntPtr)(sb + 0xA8));
|
||
if (!string.IsNullOrWhiteSpace(h))
|
||
return h.Trim();
|
||
string? p = Marshal.PtrToStringUTF8((IntPtr)(sb + 0x88));
|
||
if (string.IsNullOrWhiteSpace(p)) return "";
|
||
p = p!.Trim();
|
||
if (p.StartsWith(" [", StringComparison.Ordinal) && p.EndsWith("]", StringComparison.Ordinal))
|
||
p = p[2..^1].Trim();
|
||
return p;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Reads hotbar slot data from RaptureHotbarModule. Returns up to slotCount slots.
|
||
/// hotbarIndex 1-10 maps to StandardHotbars 0-9.
|
||
/// </summary>
|
||
public unsafe List<SlotInfo> GetSlotData(int hotbarIndex, int slotCount)
|
||
{
|
||
var list = new List<SlotInfo>(slotCount);
|
||
var module = RaptureHotbarModule.Instance();
|
||
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);
|
||
|
||
ref var bar = ref hotbars[barIdx];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var slot = bar.GetHotbarSlot((uint)i);
|
||
string keybind = ReadKeybindHintFromSlot(slot);
|
||
|
||
if (slot == null)
|
||
{
|
||
list.Add(new SlotInfo(0, true, false, 0, 0, 0, 0, keybind));
|
||
continue;
|
||
}
|
||
|
||
if (slot->IsEmpty)
|
||
{
|
||
list.Add(new SlotInfo(0, true, false, 0, 0, 0, 0, keybind));
|
||
continue;
|
||
}
|
||
|
||
bool usable = slot->IsSlotUsable(slot->ApparentSlotType, slot->ApparentActionId);
|
||
uint iconId = slot->IconId;
|
||
uint actionId = slot->ApparentActionId;
|
||
var slotType = slot->ApparentSlotType;
|
||
|
||
(int pct, int secsLeft) = GetSlotCooldown(slot);
|
||
list.Add(new SlotInfo(iconId, false, usable, pct, secsLeft, actionId, slotType, keybind));
|
||
}
|
||
|
||
return list;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets cooldown for a hotbar slot. For Action/GeneralAction/PetAction, uses ActionManager recast API
|
||
/// (more accurate for adjusted IDs, recast groups). Falls back to slot's GetSlotActionCooldownPercentage for Items/Macros.
|
||
/// </summary>
|
||
private static unsafe (int CooldownPercent, int SecondsLeft) GetSlotCooldown(RaptureHotbarModule.HotbarSlot* slot)
|
||
{
|
||
if (slot == null) return (0, 0);
|
||
|
||
var slotType = slot->ApparentSlotType;
|
||
uint actionId = slot->ApparentActionId;
|
||
if (actionId == 0) return GetSlotCooldownFromSlot(slot);
|
||
|
||
var actionManager = ActionManager.Instance();
|
||
if (actionManager == null) return GetSlotCooldownFromSlot(slot);
|
||
|
||
ActionType? actionType = slotType switch
|
||
{
|
||
RaptureHotbarModule.HotbarSlotType.Action => ActionType.Action,
|
||
RaptureHotbarModule.HotbarSlotType.GeneralAction => ActionType.GeneralAction,
|
||
RaptureHotbarModule.HotbarSlotType.PetAction => ActionType.PetAction,
|
||
RaptureHotbarModule.HotbarSlotType.CraftAction => ActionType.CraftAction,
|
||
RaptureHotbarModule.HotbarSlotType.Item => ActionType.Item,
|
||
_ => null
|
||
};
|
||
|
||
if (actionType.HasValue)
|
||
{
|
||
// GetAdjustedActionId resolves Continuation, Egi Assaults, etc. Only applies to Action type
|
||
uint effectiveId = actionType.Value == ActionType.Action
|
||
? actionManager->GetAdjustedActionId(actionId)
|
||
: actionId;
|
||
float total = actionManager->GetRecastTime(actionType.Value, effectiveId);
|
||
float elapsed = actionManager->GetRecastTimeElapsed(actionType.Value, effectiveId);
|
||
|
||
if (total > 0.001f && elapsed < total)
|
||
{
|
||
float remaining = total - elapsed;
|
||
int pct = (int)Math.Clamp((remaining / total) * 100f, 0, 100);
|
||
int secsLeft = (int)Math.Ceiling(remaining);
|
||
return (pct, secsLeft);
|
||
}
|
||
// Cooldown complete (elapsed >= total). Return 0 — do NOT fall through to GetSlotCooldownFromSlot,
|
||
// as the slot can report a stale/false cooldown (e.g. shared recast group or cached state).
|
||
if (total > 0.001f)
|
||
return (0, 0);
|
||
}
|
||
|
||
return GetSlotCooldownFromSlot(slot);
|
||
}
|
||
|
||
private static unsafe (int CooldownPercent, int SecondsLeft) GetSlotCooldownFromSlot(RaptureHotbarModule.HotbarSlot* slot)
|
||
{
|
||
if (slot == null) return (0, 0);
|
||
int secsLeft = 0;
|
||
int pct = slot->GetSlotActionCooldownPercentage(&secsLeft, 0);
|
||
return (pct, secsLeft);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Returns the default game keybind label for a hotbar slot (Hotbar 1: 1,2,...,0,-,=; Bar 2: Ctrl+1..12; etc.).
|
||
/// hotbarIndex 1–10, slotIndex 0–11. Used to mirror the default hotbar keybind display.
|
||
/// </summary>
|
||
public static string GetDefaultKeybindLabel(int hotbarIndex, int slotIndex)
|
||
{
|
||
int s = Math.Clamp(slotIndex, 0, 11);
|
||
string k = s switch
|
||
{
|
||
0 => "1", 1 => "2", 2 => "3", 3 => "4", 4 => "5", 5 => "6",
|
||
6 => "7", 7 => "8", 8 => "9", 9 => "0", 10 => "-", 11 => "=",
|
||
_ => (s + 1).ToString()
|
||
};
|
||
int b = Math.Clamp(hotbarIndex, 1, 10);
|
||
return b switch
|
||
{
|
||
1 => k,
|
||
2 => "Ctrl+" + k,
|
||
3 => "Shift+" + k,
|
||
4 => "Alt+" + k,
|
||
_ => $"{b}-{s + 1}"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Execute a hotbar slot. hotbarIndex 1-10, slotIndex 0-based.
|
||
/// </summary>
|
||
public unsafe bool ExecuteSlot(int hotbarIndex, int slotIndex)
|
||
{
|
||
var module = RaptureHotbarModule.Instance();
|
||
if (module == null || !module->ModuleReady)
|
||
return false;
|
||
|
||
int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
|
||
int slot = Math.Clamp(slotIndex, 0, 11);
|
||
module->ExecuteSlotById((uint)barIdx, (uint)slot);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Swap two hotbar slots. Supports cross-hotbar swap when hotbarA != hotbarB.
|
||
/// Uses CommandType/CommandId from the slot (not Apparent*) so items and macros swap correctly.
|
||
/// hotbarIndex 1-10, slot indices 0-based.
|
||
/// </summary>
|
||
/// <param name="debugLog">When non-null, receives diagnostic info for logging.</param>
|
||
public unsafe bool SwapSlots(int hotbarA, int slotA, int hotbarB, int slotB, Action<string>? debugLog = null)
|
||
{
|
||
var module = RaptureHotbarModule.Instance();
|
||
if (module == null || !module->ModuleReady)
|
||
{
|
||
debugLog?.Invoke($"[SwapSlots] FAIL: module null or not ready");
|
||
return false;
|
||
}
|
||
|
||
int barA = Math.Clamp(hotbarA, 1, 10);
|
||
int barB = Math.Clamp(hotbarB, 1, 10);
|
||
int a = Math.Clamp(slotA, 0, 11);
|
||
int b = Math.Clamp(slotB, 0, 11);
|
||
if (barA == barB && a == b)
|
||
{
|
||
debugLog?.Invoke($"[SwapSlots] NOOP: same slot bar={barA} slot={a}");
|
||
return true;
|
||
}
|
||
|
||
var slotPtrA = module->GetSlotById((uint)(barA - 1), (uint)a);
|
||
var slotPtrB = module->GetSlotById((uint)(barB - 1), (uint)b);
|
||
if (slotPtrA == null || slotPtrB == null)
|
||
{
|
||
debugLog?.Invoke($"[SwapSlots] FAIL: slotPtr null A={slotPtrA != null} B={slotPtrB != null}");
|
||
return false;
|
||
}
|
||
|
||
var typeA = slotPtrA->IsEmpty ? RaptureHotbarModule.HotbarSlotType.Empty : slotPtrA->CommandType;
|
||
var idA = slotPtrA->IsEmpty ? 0u : slotPtrA->CommandId;
|
||
var typeB = slotPtrB->IsEmpty ? RaptureHotbarModule.HotbarSlotType.Empty : slotPtrB->CommandType;
|
||
var idB = slotPtrB->IsEmpty ? 0u : slotPtrB->CommandId;
|
||
|
||
debugLog?.Invoke($"[SwapSlots] BEFORE: A(bar{barA} slot{a}) type={typeA} id={idA} | B(bar{barB} slot{b}) type={typeB} id={idB}");
|
||
|
||
// Update live slots first (like game drag-drop), then persist
|
||
slotPtrA->Set(typeB, idB);
|
||
slotPtrA->LoadIconId();
|
||
if (typeB == RaptureHotbarModule.HotbarSlotType.Item)
|
||
slotPtrA->LoadCostDataForSlot(true);
|
||
slotPtrB->Set(typeA, idA);
|
||
slotPtrB->LoadIconId();
|
||
if (typeA == RaptureHotbarModule.HotbarSlotType.Item)
|
||
slotPtrB->LoadCostDataForSlot(true);
|
||
module->SetAndSaveSlot((uint)(barA - 1), (uint)a, typeB, idB);
|
||
module->SetAndSaveSlot((uint)(barB - 1), (uint)b, typeA, idA);
|
||
|
||
// Read back to verify
|
||
slotPtrA = module->GetSlotById((uint)(barA - 1), (uint)a);
|
||
slotPtrB = module->GetSlotById((uint)(barB - 1), (uint)b);
|
||
if (slotPtrA != null && slotPtrB != null)
|
||
{
|
||
var afterA = slotPtrA->IsEmpty ? "empty" : $"{slotPtrA->CommandType} id={slotPtrA->CommandId}";
|
||
var afterB = slotPtrB->IsEmpty ? "empty" : $"{slotPtrB->CommandType} id={slotPtrB->CommandId}";
|
||
debugLog?.Invoke($"[SwapSlots] AFTER: A(bar{barA} slot{a}) {afterA} | B(bar{barB} slot{b}) {afterB}");
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// <summary>Dump HotbarSlot and RaptureMacroModule.Macro memory for a slot (for macro persistence debugging).</summary>
|
||
/// <param name="hotbarIndex">1-10</param>
|
||
/// <param name="slotIndex">0-11</param>
|
||
public unsafe void DumpMacroSlotMemoryToLog(int hotbarIndex, int slotIndex)
|
||
{
|
||
var hotbarModule = RaptureHotbarModule.Instance();
|
||
if (hotbarModule == null || !hotbarModule->ModuleReady)
|
||
{
|
||
Plugin.Logger.Information("[HSUI Macro DBG] RaptureHotbarModule not ready");
|
||
return;
|
||
}
|
||
int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
|
||
int slot = Math.Clamp(slotIndex, 0, 11);
|
||
var slotPtr = hotbarModule->GetSlotById((uint)barIdx, (uint)slot);
|
||
if (slotPtr == null)
|
||
{
|
||
Plugin.Logger.Information($"[HSUI Macro DBG] Bar {hotbarIndex} slot {slotIndex}: slot is null");
|
||
return;
|
||
}
|
||
var ct = slotPtr->CommandType;
|
||
var cid = slotPtr->CommandId;
|
||
Plugin.Logger.Information($"[HSUI Macro DBG] Bar {hotbarIndex} slot {slotIndex}: CommandType={ct} CommandId={cid} ApparentActionId={slotPtr->ApparentActionId} ApparentSlotType={slotPtr->ApparentSlotType} IconId={slotPtr->IconId}");
|
||
if (ct == RaptureHotbarModule.HotbarSlotType.Macro && cid is >= 1 and <= 200)
|
||
{
|
||
byte macroSet = (byte)((cid - 1) / 100);
|
||
byte macroIdx0Based = (byte)((cid - 1) % 100);
|
||
uint macroIdx1Based = (uint)(macroIdx0Based + 1); // GetMacro expects 1-based index
|
||
var macroModule = RaptureMacroModule.Instance();
|
||
if (macroModule != null)
|
||
{
|
||
var macro = macroModule->GetMacro(macroSet, macroIdx1Based);
|
||
if (macro != null)
|
||
{
|
||
string name = macro->Name.ToString() ?? "(null)";
|
||
Plugin.Logger.Information($"[HSUI Macro DBG] RaptureMacroModule.Macro set={macroSet} idx1Based={macroIdx1Based}: IconId={macro->IconId} MacroIconRowId={macro->MacroIconRowId} Name='{name}'");
|
||
|
||
// Raw byte dump (first 0x80 bytes: IconId, MacroIconRowId, start of Name/Utf8String)
|
||
var sb = new StringBuilder();
|
||
var ptr = (byte*)macro;
|
||
for (int i = 0; i < 0x80 && i < 0x688; i += 16)
|
||
{
|
||
sb.Clear();
|
||
sb.Append($"[HSUI Macro DBG] Macro+0x{i:X3}:");
|
||
for (int j = 0; j < 16 && i + j < 0x80; j++)
|
||
sb.Append($" {(ptr[i + j]):X2}");
|
||
Plugin.Logger.Information(sb.ToString());
|
||
}
|
||
|
||
// Try AddonMacro _macroName (offset 0x798, Utf8String=0x68 each, 100 entries)
|
||
try
|
||
{
|
||
var addonAddr = Plugin.GameGui?.GetAddonByName("Macro", 1).Address ?? IntPtr.Zero;
|
||
if (addonAddr != IntPtr.Zero)
|
||
{
|
||
var addon = (AddonMacro*)addonAddr;
|
||
int utf8Size = 0x68;
|
||
int nameBase = 0x798;
|
||
var namePtr = (Utf8String*)((byte*)addon + nameBase + macroIdx0Based * utf8Size);
|
||
string addonName = namePtr->ToString() ?? "(null)";
|
||
Plugin.Logger.Information($"[HSUI Macro DBG] AddonMacro._macroName[{macroIdx0Based}]: '{addonName}'");
|
||
}
|
||
else
|
||
Plugin.Logger.Information("[HSUI Macro DBG] AddonMacro not found or not open");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Plugin.Logger.Information($"[HSUI Macro DBG] AddonMacro read failed: {ex.Message}");
|
||
}
|
||
|
||
// Try AgentMacro SelectedMacroSet/Index (only relevant when that macro is selected)
|
||
try
|
||
{
|
||
var agentModule = FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentModule.Instance();
|
||
if (agentModule != null)
|
||
{
|
||
var macroAgent = agentModule->GetAgentByInternalId(FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentId.Macro);
|
||
if (macroAgent != null)
|
||
{
|
||
var agentMacro = (FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentMacro*)macroAgent;
|
||
Plugin.Logger.Information($"[HSUI Macro DBG] AgentMacro SelectedMacroSet={agentMacro->SelectedMacroSet} SelectedMacroIndex={agentMacro->SelectedMacroIndex}");
|
||
if (agentMacro->SelectedMacroSet == macroSet && agentMacro->SelectedMacroIndex == macroIdx1Based)
|
||
{
|
||
string rawStr = agentMacro->RawMacroString.ToString() ?? "(null)";
|
||
Plugin.Logger.Information($"[HSUI Macro DBG] AgentMacro (matches): RawMacroString(len={rawStr.Length})='{(rawStr.Length > 60 ? rawStr[..60] + "..." : rawStr)}'");
|
||
}
|
||
}
|
||
else
|
||
Plugin.Logger.Information("[HSUI Macro DBG] AgentMacro agent is null");
|
||
}
|
||
else
|
||
Plugin.Logger.Information("[HSUI Macro DBG] AgentModule.Instance() is null");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Plugin.Logger.Information($"[HSUI Macro DBG] AgentMacro read failed: {ex.Message}");
|
||
}
|
||
}
|
||
else
|
||
Plugin.Logger.Information($"[HSUI Macro DBG] RaptureMacroModule.GetMacro({macroSet},{macroIdx1Based}) returned null");
|
||
}
|
||
else
|
||
Plugin.Logger.Information("[HSUI Macro DBG] RaptureMacroModule.Instance() is null");
|
||
}
|
||
}
|
||
|
||
/// <summary>Dump AddonMacro and AgentMacro state (open Macro menu first for best data).</summary>
|
||
public unsafe void DumpMacroMenuToLog()
|
||
{
|
||
Plugin.Logger.Information("[HSUI MacroMenu DBG] === Macro menu debug ===");
|
||
|
||
// AddonMacro
|
||
try
|
||
{
|
||
var addonAddr = Plugin.GameGui?.GetAddonByName("Macro", 1).Address ?? IntPtr.Zero;
|
||
if (addonAddr != IntPtr.Zero)
|
||
{
|
||
var addon = (AddonMacro*)addonAddr;
|
||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] AddonMacro: SelectedPage={addon->SelectedPage} SelectedMacroIndex={addon->SelectedMacroIndex} DefaultIcon={addon->DefaultIcon}");
|
||
const int utf8Size = 0x68;
|
||
const int nameBase = 0x798;
|
||
const int iconBase = 0x604;
|
||
const int createdBase = 0x3038;
|
||
for (int i = 0; i < 10; i++)
|
||
{
|
||
var namePtr = (Utf8String*)((byte*)addon + nameBase + i * utf8Size);
|
||
int icon = *(int*)((byte*)addon + iconBase + i * 4);
|
||
bool created = *((byte*)addon + createdBase + i) != 0;
|
||
string name = namePtr->ToString() ?? "(empty)";
|
||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] AddonMacro[{i}]: Name='{name}' Icon={icon} Created={created}");
|
||
}
|
||
Plugin.Logger.Information("[HSUI MacroMenu DBG] ... (showing first 10 of 100)");
|
||
}
|
||
else
|
||
Plugin.Logger.Information("[HSUI MacroMenu DBG] AddonMacro not found - open Macro menu (Character Config > Hotbars > Macros) first");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] AddonMacro failed: {ex.Message}");
|
||
}
|
||
|
||
// AgentMacro
|
||
try
|
||
{
|
||
var agentModule = FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentModule.Instance();
|
||
if (agentModule != null)
|
||
{
|
||
var macroAgent = agentModule->GetAgentByInternalId(FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentId.Macro);
|
||
if (macroAgent != null)
|
||
{
|
||
var agent = (FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentMacro*)macroAgent;
|
||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] AgentMacro: SelectedMacroSet={agent->SelectedMacroSet} (0=Individual,1=Shared) SelectedMacroIndex={agent->SelectedMacroIndex}");
|
||
string raw = agent->RawMacroString.ToString() ?? "(null)";
|
||
string parsed = agent->ParsedMacroString.ToString() ?? "(null)";
|
||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] RawMacroString(len={raw.Length}): '{(raw.Length > 80 ? raw[..80] + "..." : raw)}'");
|
||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] ParsedMacroString(len={parsed.Length}): '{(parsed.Length > 80 ? parsed[..80] + "..." : parsed)}'");
|
||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] MacroIconCount={agent->MacroIconCount}");
|
||
|
||
var clip = &agent->ClipboardMacro;
|
||
string clipName = clip->Name.ToString() ?? "(null)";
|
||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] ClipboardMacro: IconId={clip->IconId} MacroIconRowId={clip->MacroIconRowId} Name='{clipName}'");
|
||
|
||
var rm = RaptureMacroModule.Instance();
|
||
if (rm != null)
|
||
{
|
||
var selectedMacro = rm->GetMacro(agent->SelectedMacroSet, agent->SelectedMacroIndex);
|
||
if (selectedMacro != null)
|
||
{
|
||
string rName = selectedMacro->Name.ToString() ?? "(null)";
|
||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] RaptureMacroModule.GetMacro({agent->SelectedMacroSet},{agent->SelectedMacroIndex}): IconId={selectedMacro->IconId} MacroIconRowId={selectedMacro->MacroIconRowId} Name='{rName}'");
|
||
}
|
||
else
|
||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] RaptureMacroModule.GetMacro returned null");
|
||
}
|
||
}
|
||
else
|
||
Plugin.Logger.Information("[HSUI MacroMenu DBG] AgentMacro agent is null");
|
||
}
|
||
else
|
||
Plugin.Logger.Information("[HSUI MacroMenu DBG] AgentModule.Instance() is null");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Plugin.Logger.Information($"[HSUI MacroMenu DBG] AgentMacro failed: {ex.Message}");
|
||
}
|
||
|
||
Plugin.Logger.Information("[HSUI MacroMenu DBG] === end ===");
|
||
}
|
||
|
||
/// <summary>Dump all hotbar slot CommandType/CommandId to the log for SwapSlots diagnosis.</summary>
|
||
public unsafe void DumpSlotStateToLog()
|
||
{
|
||
var module = RaptureHotbarModule.Instance();
|
||
if (module == null || !module->ModuleReady) { Plugin.Logger.Information("[HSUI HotbarSlots] Module not ready"); return; }
|
||
for (int bar = 1; bar <= 10; bar++)
|
||
{
|
||
var sb = new System.Text.StringBuilder();
|
||
for (int s = 0; s < 12; s++)
|
||
{
|
||
var slot = module->GetSlotById((uint)(bar - 1), (uint)s);
|
||
if (slot == null) { sb.Append("? "); continue; }
|
||
if (slot->IsEmpty) { sb.Append("-- "); continue; }
|
||
sb.Append($"{slot->CommandType}:{slot->CommandId} ");
|
||
}
|
||
Plugin.Logger.Information($"[HSUI HotbarSlots] Bar {bar}: {sb}");
|
||
}
|
||
}
|
||
|
||
/// <summary>Clear a hotbar slot. hotbarIndex 1-10, slotIndex 0-based.</summary>
|
||
public unsafe bool ClearSlot(int hotbarIndex, int slotIndex)
|
||
{
|
||
var module = RaptureHotbarModule.Instance();
|
||
if (module == null || !module->ModuleReady) return false;
|
||
int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
|
||
int slot = Math.Clamp(slotIndex, 0, 11);
|
||
var slotPtr = module->GetSlotById((uint)barIdx, (uint)slot);
|
||
if (slotPtr != null)
|
||
{
|
||
slotPtr->Set(RaptureHotbarModule.HotbarSlotType.Empty, 0);
|
||
slotPtr->LoadIconId();
|
||
}
|
||
module->SetAndSaveSlot((uint)barIdx, (uint)slot, RaptureHotbarModule.HotbarSlotType.Empty, 0);
|
||
return true;
|
||
}
|
||
}
|
||
}
|