feat: Controller hotbars with cross layout, separate storage, and sync with game
- Add controller hotbars: 8 cross bars (L2/R2 style), separate from normal hotbars 1-8 - Controller bar slot data stored in config (not game StandardHotbars) so layouts can differ per mode - Drag-and-drop on controller bars: from game, shift+drag rearrange, release outside to clear - Independent controller bar keybinds with modifier+trigger combinations (e.g. L2+South) - Optional 'Sync bar mode with game client': follow Character Config Mouse/Gamepad toggle (PadMode) - Clone/copy actions: normal hotbars ↔ controller bars - Restore controller bar layout button; deploy to devPlugins on Release build Made-with: Cursor
This commit is contained in:
@@ -7,6 +7,8 @@ using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using HSUI.Config;
|
||||
using HSUI.Interface.GeneralElements;
|
||||
using KamiToolKit.Controllers;
|
||||
using static FFXIVClientStructs.FFXIV.Client.Game.ActionManager;
|
||||
|
||||
@@ -192,6 +194,148 @@ namespace HSUI.Helpers
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>Scratch bar/slot used to build SlotInfo or execute from (type, id) without touching game bars 1-8. Bar 10 slot 0.</summary>
|
||||
internal const int ScratchBarIndex = 10;
|
||||
internal const int ScratchSlotIndex = 0;
|
||||
|
||||
/// <summary>Get SlotInfo for a single (type, id) by temporarily writing to scratch slot and reading. Used for controller bars.</summary>
|
||||
private unsafe SlotInfo GetSlotInfoViaScratch(RaptureHotbarModule.HotbarSlotType slotType, uint commandId)
|
||||
{
|
||||
var module = RaptureHotbarModule.Instance();
|
||||
if (module == null || !module->ModuleReady)
|
||||
return new SlotInfo(0, true, false, 0, 0, 0, 0, "", 0, 0);
|
||||
int barIdx = ScratchBarIndex - 1;
|
||||
var slotPtr = module->GetSlotById((uint)barIdx, (uint)ScratchSlotIndex);
|
||||
if (slotPtr == null)
|
||||
return new SlotInfo(0, true, false, 0, 0, 0, 0, "", 0, 0);
|
||||
uint saveType = (uint)slotPtr->CommandType;
|
||||
uint saveId = slotPtr->CommandId;
|
||||
try
|
||||
{
|
||||
slotPtr->Set(slotType, commandId);
|
||||
slotPtr->LoadIconId();
|
||||
if (slotType == RaptureHotbarModule.HotbarSlotType.Item)
|
||||
slotPtr->LoadCostDataForSlot(true);
|
||||
var list = GetSlotData(ScratchBarIndex, 1);
|
||||
return list.Count > 0 ? list[0] : new SlotInfo(0, true, false, 0, 0, 0, 0, "", 0, 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
slotPtr->Set((RaptureHotbarModule.HotbarSlotType)saveType, saveId);
|
||||
slotPtr->LoadIconId();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get slot data for a controller cross bar (1-8) from persisted controller storage. Returns 8 SlotInfos.</summary>
|
||||
public unsafe List<SlotInfo> GetControllerSlotData(int barIndex, int slotCount)
|
||||
{
|
||||
var list = new List<SlotInfo>(slotCount);
|
||||
var barConfig = ControllerBarConfig.GetBarConfig(barIndex);
|
||||
if (barConfig == null)
|
||||
{
|
||||
for (int i = 0; i < slotCount; i++)
|
||||
list.Add(new SlotInfo(0, true, false, 0, 0, 0, 0, "", 0, 0));
|
||||
return list;
|
||||
}
|
||||
barConfig.EnsureSlotsPadded();
|
||||
int count = Math.Min(slotCount, barConfig.Slots.Count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entry = barConfig.Slots[i];
|
||||
var type = (RaptureHotbarModule.HotbarSlotType)entry.SlotType;
|
||||
uint id = entry.CommandId;
|
||||
if (entry.IsEmpty)
|
||||
{
|
||||
list.Add(new SlotInfo(0, true, false, 0, 0, 0, 0, "", 0, 0));
|
||||
continue;
|
||||
}
|
||||
list.Add(GetSlotInfoViaScratch(type, id));
|
||||
}
|
||||
while (list.Count < slotCount)
|
||||
list.Add(new SlotInfo(0, true, false, 0, 0, 0, 0, "", 0, 0));
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>Place payload on a controller bar slot (persisted in config only). Bar 1-8, slot 0-7.</summary>
|
||||
public void PlacePayloadOnControllerSlot(int barIndex, int slotIndex, RaptureHotbarModule.HotbarSlotType slotType, uint commandId)
|
||||
{
|
||||
var barConfig = ControllerBarConfig.GetBarConfig(barIndex);
|
||||
if (barConfig == null) return;
|
||||
barConfig.EnsureSlotsPadded();
|
||||
int s = Math.Clamp(slotIndex, 0, ControllerBarConfig.CrossSlotCount - 1);
|
||||
while (barConfig.Slots.Count <= s)
|
||||
barConfig.Slots.Add(new ControllerSlotData { SlotType = (int)RaptureHotbarModule.HotbarSlotType.Empty, CommandId = 0 });
|
||||
barConfig.Slots[s] = new ControllerSlotData { SlotType = (int)slotType, CommandId = commandId };
|
||||
ConfigurationManager.Instance?.SaveConfigurations();
|
||||
}
|
||||
|
||||
/// <summary>Clear a controller bar slot. Bar 1-8, slot 0-7.</summary>
|
||||
public bool ClearControllerSlot(int barIndex, int slotIndex)
|
||||
{
|
||||
var barConfig = ControllerBarConfig.GetBarConfig(barIndex);
|
||||
if (barConfig == null) return false;
|
||||
barConfig.EnsureSlotsPadded();
|
||||
int s = Math.Clamp(slotIndex, 0, ControllerBarConfig.CrossSlotCount - 1);
|
||||
if (s >= barConfig.Slots.Count) return true;
|
||||
barConfig.Slots[s] = new ControllerSlotData { SlotType = (int)RaptureHotbarModule.HotbarSlotType.Empty, CommandId = 0 };
|
||||
ConfigurationManager.Instance?.SaveConfigurations();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Swap two controller bar slots. Bars 1-8, slots 0-7.</summary>
|
||||
public bool SwapControllerSlots(int barA, int slotA, int barB, int slotB)
|
||||
{
|
||||
var configA = ControllerBarConfig.GetBarConfig(barA);
|
||||
var configB = ControllerBarConfig.GetBarConfig(barB);
|
||||
if (configA == null || configB == null) return false;
|
||||
configA.EnsureSlotsPadded();
|
||||
configB.EnsureSlotsPadded();
|
||||
int a = Math.Clamp(slotA, 0, ControllerBarConfig.CrossSlotCount - 1);
|
||||
int b = Math.Clamp(slotB, 0, ControllerBarConfig.CrossSlotCount - 1);
|
||||
if (barA == barB && a == b) return true;
|
||||
if (a >= configA.Slots.Count || b >= configB.Slots.Count) return false;
|
||||
var tmp = configA.Slots[a];
|
||||
configA.Slots[a] = configB.Slots[b];
|
||||
configB.Slots[b] = tmp;
|
||||
ConfigurationManager.Instance?.SaveConfigurations();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Execute a controller bar slot by (type, id) via scratch slot. Bar 1-8, slot 0-7.</summary>
|
||||
public unsafe bool ExecuteControllerSlot(int barIndex, int slotIndex)
|
||||
{
|
||||
var barConfig = ControllerBarConfig.GetBarConfig(barIndex);
|
||||
if (barConfig == null) return false;
|
||||
barConfig.EnsureSlotsPadded();
|
||||
int s = Math.Clamp(slotIndex, 0, ControllerBarConfig.CrossSlotCount - 1);
|
||||
if (s >= barConfig.Slots.Count) return false;
|
||||
var entry = barConfig.Slots[s];
|
||||
if (entry.IsEmpty) return false;
|
||||
var slotType = (RaptureHotbarModule.HotbarSlotType)entry.SlotType;
|
||||
uint commandId = entry.CommandId;
|
||||
|
||||
var module = RaptureHotbarModule.Instance();
|
||||
if (module == null || !module->ModuleReady) return false;
|
||||
var slotPtr = module->GetSlotById((uint)(ScratchBarIndex - 1), (uint)ScratchSlotIndex);
|
||||
if (slotPtr == null) return false;
|
||||
uint saveType = (uint)slotPtr->CommandType;
|
||||
uint saveId = slotPtr->CommandId;
|
||||
try
|
||||
{
|
||||
slotPtr->Set(slotType, commandId);
|
||||
slotPtr->LoadIconId();
|
||||
if (slotType == RaptureHotbarModule.HotbarSlotType.Item)
|
||||
slotPtr->LoadCostDataForSlot(true);
|
||||
module->ExecuteSlotById((uint)(ScratchBarIndex - 1), (uint)ScratchSlotIndex);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
slotPtr->Set((RaptureHotbarModule.HotbarSlotType)saveType, saveId);
|
||||
slotPtr->LoadIconId();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.
|
||||
@@ -581,7 +725,23 @@ namespace HSUI.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Clear a hotbar slot. hotbarIndex 1-10, slotIndex 0-based.</summary>
|
||||
/// <summary>Place a drag-drop payload (game or HSUI) onto a slot. hotbarIndex 1-10, slotIndex 0-based.</summary>
|
||||
public static unsafe void PlacePayloadOnSlot(int hotbarIndex, int slotIndex, RaptureHotbarModule.HotbarSlotType slotType, uint commandId)
|
||||
{
|
||||
var module = RaptureHotbarModule.Instance();
|
||||
if (module == null || !module->ModuleReady) return;
|
||||
int barIdx = Math.Clamp(hotbarIndex, 1, 10) - 1;
|
||||
int slot = Math.Clamp(slotIndex, 0, 11);
|
||||
ref var displayBar = ref module->StandardHotbars[barIdx];
|
||||
var slotPtr = displayBar.GetHotbarSlot((uint)slot);
|
||||
if (slotPtr == null) return;
|
||||
slotPtr->Set(slotType, commandId);
|
||||
slotPtr->LoadIconId();
|
||||
if (slotType == RaptureHotbarModule.HotbarSlotType.Item)
|
||||
slotPtr->LoadCostDataForSlot(true);
|
||||
SetAndSaveSlotInternal(module, (uint)barIdx, (uint)slot, slotType, commandId, slotPtr);
|
||||
}
|
||||
|
||||
public unsafe bool ClearSlot(int hotbarIndex, int slotIndex)
|
||||
{
|
||||
var module = RaptureHotbarModule.Instance();
|
||||
|
||||
Reference in New Issue
Block a user