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!; } /// /// Slot data for ImGui drawing. Updated each call from game state. /// 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; } /// Keybind hint from game (user's keybind settings). Empty if unavailable. public string KeybindHint { get; } /// Current charges for charge-based actions; 0 when not applicable. public int CurrentCharges { get; } /// Max charges for charge-based actions; >1 only for actions with charges. public int MaxCharges { get; } public SlotInfo(uint iconId, bool isEmpty, bool usable, int cooldownPct, int cooldownSecs, uint actionId = 0, RaptureHotbarModule.HotbarSlotType slotType = 0, string keybindHint = "", int currentCharges = 0, int maxCharges = 0) { IconId = iconId; IsEmpty = isEmpty; IsUsable = usable; CooldownPercent = cooldownPct; CooldownSecondsLeft = cooldownSecs; ActionId = actionId; SlotType = slotType; KeybindHint = keybindHint ?? ""; CurrentCharges = currentCharges; MaxCharges = maxCharges; } } /// /// Reads keybind hint from HotbarSlot. Uses _keybindHint (slot display) then _popUpKeybindHint (trimmed) as fallback. /// Offsets match RaptureHotbarModule.HotbarSlot: _keybindHint 0xA8, _popUpKeybindHint 0x88. /// 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; } /// /// Reads hotbar slot data from RaptureHotbarModule. Returns up to slotCount slots. /// hotbarIndex 1-10 maps to StandardHotbars 0-9. /// public unsafe List GetSlotData(int hotbarIndex, int slotCount) { var list = new List(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, 0, 0)); continue; } if (slot->IsEmpty) { list.Add(new SlotInfo(0, true, false, 0, 0, 0, 0, keybind, 0, 0)); 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); (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; list.Add(new SlotInfo(iconId, false, usable, pct, secsLeft, actionId, slotType, keybind, currentCharges, maxCharges)); } return list; } /// /// 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. /// /// /// Gets current and max charges for a slot. Only applies to Action type; returns (0,0) otherwise. /// private static unsafe (int CurrentCharges, int MaxCharges) GetSlotCharges(RaptureHotbarModule.HotbarSlotType slotType, uint actionId) { if (slotType != RaptureHotbarModule.HotbarSlotType.Action || actionId == 0) return (0, 0); var actionManager = ActionManager.Instance(); if (actionManager == null) return (0, 0); uint effectiveId = actionManager->GetAdjustedActionId(actionId); ushort maxCh = ActionManager.GetMaxCharges(effectiveId, 0); if (maxCh <= 1) return (0, 0); uint currentCh = actionManager->GetCurrentCharges(effectiveId); return ((int)currentCh, (int)maxCh); } 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); } /// /// 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. /// 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}" }; } /// /// Execute a hotbar slot. hotbarIndex 1-10, slotIndex 0-based. /// 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; } /// /// 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. /// /// When non-null, receives diagnostic info for logging. public unsafe bool SwapSlots(int hotbarA, int slotA, int hotbarB, int slotB, Action? 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); SetAndSaveSlotInternal(module, (uint)(barA - 1), (uint)a, typeB, idB, slotPtrA); SetAndSaveSlotInternal(module, (uint)(barB - 1), (uint)b, typeA, idA, slotPtrB); // 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; } /// Dump HotbarSlot and RaptureMacroModule.Macro memory for a slot (for macro persistence debugging). /// 1-10 /// 0-11 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"); } } /// Dump AddonMacro and AgentMacro state (open Macro menu first for best data). 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 ==="); } /// Dump all hotbar slot CommandType/CommandId to the log for SwapSlots diagnosis. 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}"); } } /// Clear a hotbar slot. hotbarIndex 1-10, slotIndex 0-based. 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(); } SetAndSaveSlotInternal(module, (uint)barIdx, (uint)slot, RaptureHotbarModule.HotbarSlotType.Empty, 0, slotPtr); return true; } /// Sets a slot and persists to disk. For shared hotbars, explicitly writes to classJobId 0. /// For job-specific hotbars, explicitly writes to current class job ID to ensure persistence when switching jobs. public static unsafe void SetAndSaveSlotInternal(RaptureHotbarModule* module, uint barId, uint slotId, RaptureHotbarModule.HotbarSlotType slotType, uint commandId, RaptureHotbarModule.HotbarSlot* slotPtr = null) { var ptr = slotPtr != null ? slotPtr : module->GetSlotById(barId, slotId); if (ptr == null) return; if (module->IsHotbarShared(barId)) { // Shared hotbars: explicitly write to classJobId 0 (shared storage) for persistence across job changes and teleports module->WriteSavedSlot(0, barId, slotId, ptr, false, false); } else { // Job-specific hotbars: explicitly write to current class job ID so saves persist when switching jobs uint classJobId = (uint)(module->ActiveHotbarClassJobId & 0x7F); // strip 0x80 flag if set if (classJobId == 0) { var player = Plugin.ObjectTable?.LocalPlayer; if (player != null) classJobId = player.ClassJob.RowId; } if (classJobId != 0) module->WriteSavedSlot(classJobId, barId, slotId, ptr, false, false); } // SetAndSaveSlot updates live slot and triggers file save module->SetAndSaveSlot(barId, slotId, slotType, commandId, ignoreSharedHotbars: false, allowSaveToPvP: true); } } }