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,457 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using KamiToolKit.Controllers;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
int secsLeft = 0;
|
||||
int pct = slot->GetSlotActionCooldownPercentage(&secsLeft, 0);
|
||||
bool usable = slot->IsSlotUsable(slot->ApparentSlotType, slot->ApparentActionId);
|
||||
uint iconId = slot->IconId;
|
||||
uint actionId = slot->ApparentActionId;
|
||||
var slotType = slot->ApparentSlotType;
|
||||
|
||||
list.Add(new SlotInfo(iconId, false, usable, pct, secsLeft, actionId, slotType, keybind));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user