f3e10f27d2
- 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
449 lines
20 KiB
C#
449 lines
20 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using HSUI.Config;
|
||
using HSUI.Config.Attributes;
|
||
using HSUI.Enums;
|
||
using HSUI.Helpers;
|
||
using HSUI.Interface;
|
||
using System.Numerics;
|
||
using Dalamud.Bindings.ImGui;
|
||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||
|
||
namespace HSUI.Interface.GeneralElements
|
||
{
|
||
/// <summary>Persisted slot content for one controller cross bar slot. Not shown in config UI.</summary>
|
||
public class ControllerSlotData
|
||
{
|
||
public int SlotType { get; set; }
|
||
public uint CommandId { get; set; }
|
||
|
||
public bool IsEmpty => SlotType == (int)RaptureHotbarModule.HotbarSlotType.Empty || (SlotType != (int)RaptureHotbarModule.HotbarSlotType.Macro && SlotType != (int)RaptureHotbarModule.HotbarSlotType.GearSet && CommandId == 0);
|
||
}
|
||
|
||
/// <summary>Which trigger combo shows this cross bar. Matches FFXIV cross hotbar sets (no trigger, L2, R2, L2+R2, R2+L2, then double-tap variants).</summary>
|
||
public enum CrossBarTrigger
|
||
{
|
||
None = 0, // Bar 1: always visible when no trigger held
|
||
L2 = 1,
|
||
R2 = 2,
|
||
L2R2 = 3, // L2 then R2
|
||
R2L2 = 4, // R2 then L2
|
||
L2Double = 5,
|
||
R2Double = 6,
|
||
L2R2Double = 7,
|
||
R2L2Double = 8,
|
||
}
|
||
|
||
/// <summary>Config for one controller cross bar (8 slots in cross layout). Slot data is stored separately from game hotbars so controller and normal bars can differ.</summary>
|
||
public class ControllerBarConfig : AnchorablePluginConfigObject
|
||
{
|
||
internal int HotbarIndex { get; set; } = 1; // 1-8
|
||
|
||
/// <summary>Persisted slot contents for this cross bar (8 slots). Not drawn in config UI. Empty slots are SlotType=0 (Empty), CommandId=0.</summary>
|
||
public List<ControllerSlotData> Slots = new List<ControllerSlotData>();
|
||
|
||
[Combo("Show when", new string[] { "No trigger", "L2", "R2", "L2 + R2", "R2 + L2", "L2 double-tap", "R2 double-tap", "L2+R2 double", "R2+L2 double" })]
|
||
[Order(18)]
|
||
public int TriggerCombo = 0; // CrossBarTrigger
|
||
|
||
[DragInt2("Slot Size", min = 24, max = 96)]
|
||
[Order(22)]
|
||
public Vector2 SlotSize = new Vector2(40, 40);
|
||
|
||
[DragInt("Slot Padding", min = 0, max = 16)]
|
||
[Order(23)]
|
||
public int SlotPadding = 2;
|
||
|
||
[Checkbox("Show Cooldown Overlay")]
|
||
[Order(24)]
|
||
public bool ShowCooldownOverlay = true;
|
||
|
||
[Checkbox("Show Cooldown Numbers")]
|
||
[Order(25)]
|
||
public bool ShowCooldownNumbers = true;
|
||
|
||
[Checkbox("Show Border")]
|
||
[Order(26)]
|
||
public bool ShowBorder = true;
|
||
|
||
[Checkbox("Show Tooltips")]
|
||
[Order(27)]
|
||
public bool ShowTooltips = true;
|
||
|
||
[Checkbox("Show Keybinds")]
|
||
[Order(28)]
|
||
public bool ShowSlotNumbers = true;
|
||
|
||
[Checkbox("Show Charge Count")]
|
||
[Order(29)]
|
||
public bool ShowChargeCount = true;
|
||
|
||
[Checkbox("Show Combo Highlight")]
|
||
[Order(30)]
|
||
public bool ShowComboHighlight = true;
|
||
|
||
[Checkbox("Show Keypress Flash")]
|
||
[Order(31)]
|
||
public bool ShowKeypressFlash = true;
|
||
|
||
[NestedConfig("Combo Highlight", 32)]
|
||
public ComboHighlightConfig ComboHighlightConfig = new();
|
||
|
||
[NestedConfig("Visibility", 70)]
|
||
public VisibilityConfig VisibilityConfig = new VisibilityConfig();
|
||
|
||
/// <summary>Cross layout: 8 slots. Left 4 (vertical) + Right 4 (vertical). Slot indices 0-3 left, 4-7 right.</summary>
|
||
public const int CrossSlotCount = 8;
|
||
|
||
public CrossBarTrigger Trigger => (CrossBarTrigger)Math.Clamp(TriggerCombo, 0, 8);
|
||
|
||
public static ControllerBarConfig DefaultConfig(int hotbarIndex)
|
||
{
|
||
var config = new ControllerBarConfig { HotbarIndex = hotbarIndex };
|
||
ApplyDefaults(config, hotbarIndex);
|
||
return config;
|
||
}
|
||
|
||
public static void ApplyDefaults(ControllerBarConfig config, int hotbarIndex)
|
||
{
|
||
var viewport = ImGui.GetMainViewport().Size;
|
||
float yOffset = 60 + (hotbarIndex - 1) * 50;
|
||
config.Position = new Vector2(0, -viewport.Y * 0.5f + yOffset);
|
||
config.Anchor = DrawAnchor.Center;
|
||
// Two crosses side by side: 3 cells each (slot+pad), gap between
|
||
float cell = 40 + 2;
|
||
config.Size = new Vector2(3 * cell + 4 + 3 * cell, 3 * cell);
|
||
config.HotbarIndex = hotbarIndex;
|
||
}
|
||
|
||
/// <summary>Get the controller bar config for bar 1-8. Returns null if config not available.</summary>
|
||
public static ControllerBarConfig? GetBarConfig(int barIndex)
|
||
{
|
||
var cfg = ConfigurationManager.Instance;
|
||
if (cfg == null || barIndex < 1 || barIndex > 8) return null;
|
||
return barIndex switch
|
||
{
|
||
1 => cfg.GetConfigObject<ControllerBar1Config>(),
|
||
2 => cfg.GetConfigObject<ControllerBar2Config>(),
|
||
3 => cfg.GetConfigObject<ControllerBar3Config>(),
|
||
4 => cfg.GetConfigObject<ControllerBar4Config>(),
|
||
5 => cfg.GetConfigObject<ControllerBar5Config>(),
|
||
6 => cfg.GetConfigObject<ControllerBar6Config>(),
|
||
7 => cfg.GetConfigObject<ControllerBar7Config>(),
|
||
8 => cfg.GetConfigObject<ControllerBar8Config>(),
|
||
_ => null
|
||
};
|
||
}
|
||
|
||
/// <summary>Get slot data for this bar, padded to 8 entries. Caller can use index 0..7.</summary>
|
||
public void EnsureSlotsPadded()
|
||
{
|
||
while (Slots.Count < ControllerBarConfig.CrossSlotCount)
|
||
Slots.Add(new ControllerSlotData { SlotType = (int)RaptureHotbarModule.HotbarSlotType.Empty, CommandId = 0 });
|
||
}
|
||
}
|
||
|
||
[Exportable(false)]
|
||
[Section("Controller Hotbars", true)]
|
||
[SubSection("General", 0)]
|
||
public class ControllerHotbarsConfig : PluginConfigObject
|
||
{
|
||
[Checkbox("Use controller hotbars", help = "When enabled (and sync below is off), normal HSUI hotbars are hidden and the cross hotbars are shown. When sync with game is on, this is ignored and bar mode follows the game.")]
|
||
[Order(1, collapseWith = null)]
|
||
public new bool Enabled = false;
|
||
|
||
[Checkbox("Sync bar mode with game client", help = "When enabled, the plugin follows the game's Character Configuration toggle (Mouse and Gamepad Mode). Controller hotbars when gamepad mode is on, normal hotbars when keyboard/mouse mode is on. The 'Use controller hotbars' checkbox is ignored while sync is on.")]
|
||
[Order(2, collapseWith = null)]
|
||
public bool SyncBarModeWithGameClient = false;
|
||
|
||
/// <summary>True when controller hotbars should be shown: either sync is on and game is in controller mode, or sync is off and Enabled is true.</summary>
|
||
public static bool GetEffectiveControllerHotbarsEnabled()
|
||
{
|
||
var config = ConfigurationManager.Instance?.GetConfigObject<ControllerHotbarsConfig>();
|
||
if (config == null) return false;
|
||
if (config.SyncBarModeWithGameClient)
|
||
return ControllerHotbarHelper.IsGameInControllerMode();
|
||
return config.Enabled;
|
||
}
|
||
|
||
[ManualDraw]
|
||
public bool DrawRestoreControllerBarsButton(ref bool changed)
|
||
{
|
||
ImGuiHelper.DrawSeparator(2, 1);
|
||
if (ImGui.Button("Restore controller bar layout", new Vector2(220, 24)))
|
||
{
|
||
RestoreControllerBarsToDefaults();
|
||
changed = true;
|
||
return true;
|
||
}
|
||
if (ImGui.IsItemHovered())
|
||
ImGui.SetTooltip("Reset positions, layout, and display settings for all 8 controller cross bars to default. Use if your controller bars are misplaced or broken.");
|
||
return false;
|
||
}
|
||
|
||
[ManualDraw]
|
||
public bool DrawCloneButton(ref bool changed)
|
||
{
|
||
ImGuiHelper.DrawSeparator(2, 1);
|
||
if (ImGui.Button("Clone layout from normal hotbars", new Vector2(240, 24)))
|
||
{
|
||
CloneLayoutFromNormalHotbars();
|
||
changed = true;
|
||
return true;
|
||
}
|
||
if (ImGui.IsItemHovered())
|
||
ImGui.SetTooltip("Copy position, size, and display settings from Hotbar 1–8 to the controller cross bars. Does not change actions in the slots.");
|
||
return false;
|
||
}
|
||
|
||
[ManualDraw]
|
||
public bool DrawCloneActionsButton(ref bool changed)
|
||
{
|
||
if (ImGui.Button("Copy normal hotbars 1–8 → controller bars", new Vector2(280, 24)))
|
||
{
|
||
CloneActionsFromNormalHotbars();
|
||
changed = true;
|
||
return true;
|
||
}
|
||
if (ImGui.IsItemHovered())
|
||
ImGui.SetTooltip("Copy slot contents from normal Hotbar 1–8 into controller bar storage. Use this to mirror your keyboard hotbar setup onto the controller bars. Normal and controller bars are stored separately, so this does not overwrite your normal bars.");
|
||
return false;
|
||
}
|
||
|
||
[ManualDraw]
|
||
public bool DrawCopyControllerToNormalButton(ref bool changed)
|
||
{
|
||
if (ImGui.Button("Copy controller bars → normal hotbars 1–8", new Vector2(280, 24)))
|
||
{
|
||
CopyControllerBarsToNormalHotbars();
|
||
changed = true;
|
||
return true;
|
||
}
|
||
if (ImGui.IsItemHovered())
|
||
ImGui.SetTooltip("Copy controller bar slot contents into normal Hotbar 1–8. This overwrites the current normal hotbar 1–8 layout with your controller bar layout.");
|
||
return false;
|
||
}
|
||
|
||
public new static ControllerHotbarsConfig DefaultConfig() => new();
|
||
|
||
private static void CloneLayoutFromNormalHotbars()
|
||
{
|
||
var cfg = ConfigurationManager.Instance;
|
||
if (cfg == null) return;
|
||
var normal = new AnchorablePluginConfigObject?[]
|
||
{
|
||
cfg.GetConfigObject<Hotbar1BarConfig>(),
|
||
cfg.GetConfigObject<Hotbar2BarConfig>(),
|
||
cfg.GetConfigObject<Hotbar3BarConfig>(),
|
||
cfg.GetConfigObject<Hotbar4BarConfig>(),
|
||
cfg.GetConfigObject<Hotbar5BarConfig>(),
|
||
cfg.GetConfigObject<Hotbar6BarConfig>(),
|
||
cfg.GetConfigObject<Hotbar7BarConfig>(),
|
||
cfg.GetConfigObject<Hotbar8BarConfig>()
|
||
};
|
||
var controller = new ControllerBarConfig?[]
|
||
{
|
||
cfg.GetConfigObject<ControllerBar1Config>(),
|
||
cfg.GetConfigObject<ControllerBar2Config>(),
|
||
cfg.GetConfigObject<ControllerBar3Config>(),
|
||
cfg.GetConfigObject<ControllerBar4Config>(),
|
||
cfg.GetConfigObject<ControllerBar5Config>(),
|
||
cfg.GetConfigObject<ControllerBar6Config>(),
|
||
cfg.GetConfigObject<ControllerBar7Config>(),
|
||
cfg.GetConfigObject<ControllerBar8Config>()
|
||
};
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
if (normal[i] is HotbarBarConfig n && controller[i] is ControllerBarConfig c)
|
||
{
|
||
c.Position = n.Position;
|
||
c.Anchor = n.Anchor;
|
||
c.SlotSize = n.SlotSize;
|
||
c.SlotPadding = n.SlotPadding;
|
||
c.ShowCooldownOverlay = n.ShowCooldownOverlay;
|
||
c.ShowCooldownNumbers = n.ShowCooldownNumbers;
|
||
c.ShowBorder = n.ShowBorder;
|
||
c.ShowTooltips = n.ShowTooltips;
|
||
c.ShowSlotNumbers = n.ShowSlotNumbers;
|
||
c.ShowChargeCount = n.ShowChargeCount;
|
||
c.ShowComboHighlight = n.ShowComboHighlight;
|
||
c.ShowKeypressFlash = n.ShowKeypressFlash;
|
||
c.ComboHighlightConfig.Color = n.ComboHighlightConfig.Color;
|
||
c.ComboHighlightConfig.ShowGlow = n.ComboHighlightConfig.ShowGlow;
|
||
c.ComboHighlightConfig.LineStyle = n.ComboHighlightConfig.LineStyle;
|
||
c.ComboHighlightConfig.Thickness = n.ComboHighlightConfig.Thickness;
|
||
}
|
||
}
|
||
}
|
||
|
||
private static unsafe void CloneActionsFromNormalHotbars()
|
||
{
|
||
if (ActionBarsManager.Instance == null) return;
|
||
var module = RaptureHotbarModule.Instance();
|
||
if (module == null || !module->ModuleReady) return;
|
||
|
||
for (int bar = 1; bar <= 8; bar++)
|
||
{
|
||
var barConfig = ControllerBarConfig.GetBarConfig(bar);
|
||
if (barConfig == null) continue;
|
||
barConfig.EnsureSlotsPadded();
|
||
int barIdx = bar - 1;
|
||
ref var displayBar = ref module->StandardHotbars[barIdx];
|
||
for (int slot = 0; slot < ControllerBarConfig.CrossSlotCount; slot++)
|
||
{
|
||
var slotPtr = displayBar.GetHotbarSlot((uint)slot);
|
||
if (slotPtr == null) continue;
|
||
var slotType = slotPtr->IsEmpty && slotPtr->CommandType != RaptureHotbarModule.HotbarSlotType.GearSet
|
||
? RaptureHotbarModule.HotbarSlotType.Empty
|
||
: slotPtr->CommandType;
|
||
uint id = slotPtr->IsEmpty ? 0u : slotPtr->CommandId;
|
||
while (barConfig.Slots.Count <= slot)
|
||
barConfig.Slots.Add(new ControllerSlotData { SlotType = (int)RaptureHotbarModule.HotbarSlotType.Empty, CommandId = 0 });
|
||
barConfig.Slots[slot] = new ControllerSlotData { SlotType = (int)slotType, CommandId = id };
|
||
}
|
||
}
|
||
ConfigurationManager.Instance?.SaveConfigurations();
|
||
Plugin.Logger?.Information("[HSUI] Copied normal hotbars 1–8 to controller bar storage.");
|
||
}
|
||
|
||
private static unsafe void CopyControllerBarsToNormalHotbars()
|
||
{
|
||
if (ActionBarsManager.Instance == null) return;
|
||
var module = RaptureHotbarModule.Instance();
|
||
if (module == null || !module->ModuleReady) return;
|
||
|
||
for (int bar = 1; bar <= 8; bar++)
|
||
{
|
||
var barConfig = ControllerBarConfig.GetBarConfig(bar);
|
||
if (barConfig == null) continue;
|
||
barConfig.EnsureSlotsPadded();
|
||
int barIdx = bar - 1;
|
||
ref var displayBar = ref module->StandardHotbars[barIdx];
|
||
for (int slot = 0; slot < ControllerBarConfig.CrossSlotCount && slot < barConfig.Slots.Count; slot++)
|
||
{
|
||
var entry = barConfig.Slots[slot];
|
||
var slotType = (RaptureHotbarModule.HotbarSlotType)entry.SlotType;
|
||
uint id = entry.CommandId;
|
||
var slotPtr = displayBar.GetHotbarSlot((uint)slot);
|
||
if (slotPtr == null) continue;
|
||
slotPtr->Set(slotType, id);
|
||
slotPtr->LoadIconId();
|
||
if (slotType == RaptureHotbarModule.HotbarSlotType.Item)
|
||
slotPtr->LoadCostDataForSlot(true);
|
||
ActionBarsManager.SetAndSaveSlotInternal(module, (uint)barIdx, (uint)slot, slotType, id, slotPtr);
|
||
}
|
||
}
|
||
Plugin.Logger?.Information("[HSUI] Copied controller bar storage to normal hotbars 1–8.");
|
||
}
|
||
|
||
private static void RestoreControllerBarsToDefaults()
|
||
{
|
||
var cfg = ConfigurationManager.Instance;
|
||
if (cfg == null) return;
|
||
var bars = new (ControllerBarConfig? config, int index)[]
|
||
{
|
||
(cfg.GetConfigObject<ControllerBar1Config>(), 1),
|
||
(cfg.GetConfigObject<ControllerBar2Config>(), 2),
|
||
(cfg.GetConfigObject<ControllerBar3Config>(), 3),
|
||
(cfg.GetConfigObject<ControllerBar4Config>(), 4),
|
||
(cfg.GetConfigObject<ControllerBar5Config>(), 5),
|
||
(cfg.GetConfigObject<ControllerBar6Config>(), 6),
|
||
(cfg.GetConfigObject<ControllerBar7Config>(), 7),
|
||
(cfg.GetConfigObject<ControllerBar8Config>(), 8)
|
||
};
|
||
foreach (var (bar, idx) in bars)
|
||
{
|
||
if (bar == null) continue;
|
||
bar.Enabled = true;
|
||
ControllerBarConfig.ApplyDefaults(bar, idx);
|
||
bar.TriggerCombo = idx - 1; // Bar 1 = None(0), Bar 2 = L2(1), ... Bar 8 = R2L2Double(7)
|
||
bar.SlotSize = new Vector2(40, 40);
|
||
bar.SlotPadding = 2;
|
||
bar.ShowCooldownOverlay = true;
|
||
bar.ShowCooldownNumbers = true;
|
||
bar.ShowBorder = true;
|
||
bar.ShowTooltips = true;
|
||
bar.ShowSlotNumbers = true;
|
||
bar.ShowChargeCount = true;
|
||
bar.ShowComboHighlight = true;
|
||
bar.ShowKeypressFlash = true;
|
||
bar.ComboHighlightConfig = new ComboHighlightConfig();
|
||
bar.VisibilityConfig = new VisibilityConfig();
|
||
}
|
||
}
|
||
}
|
||
|
||
[Exportable(false)]
|
||
[Section("Controller Hotbars", true)]
|
||
[SubSection("Cross Bar 1", 0)]
|
||
public class ControllerBar1Config : ControllerBarConfig
|
||
{
|
||
public ControllerBar1Config() => HotbarIndex = 1;
|
||
public new static ControllerBar1Config DefaultConfig() { var c = new ControllerBar1Config(); ApplyDefaults(c, 1); return c; }
|
||
}
|
||
|
||
[Exportable(false)]
|
||
[Section("Controller Hotbars", true)]
|
||
[SubSection("Cross Bar 2", 0)]
|
||
public class ControllerBar2Config : ControllerBarConfig
|
||
{
|
||
public ControllerBar2Config() => HotbarIndex = 2;
|
||
public new static ControllerBar2Config DefaultConfig() { var c = new ControllerBar2Config(); ApplyDefaults(c, 2); return c; }
|
||
}
|
||
|
||
[Exportable(false)]
|
||
[Section("Controller Hotbars", true)]
|
||
[SubSection("Cross Bar 3", 0)]
|
||
public class ControllerBar3Config : ControllerBarConfig
|
||
{
|
||
public ControllerBar3Config() => HotbarIndex = 3;
|
||
public new static ControllerBar3Config DefaultConfig() { var c = new ControllerBar3Config(); ApplyDefaults(c, 3); return c; }
|
||
}
|
||
|
||
[Exportable(false)]
|
||
[Section("Controller Hotbars", true)]
|
||
[SubSection("Cross Bar 4", 0)]
|
||
public class ControllerBar4Config : ControllerBarConfig
|
||
{
|
||
public ControllerBar4Config() => HotbarIndex = 4;
|
||
public new static ControllerBar4Config DefaultConfig() { var c = new ControllerBar4Config(); ApplyDefaults(c, 4); return c; }
|
||
}
|
||
|
||
[Exportable(false)]
|
||
[Section("Controller Hotbars", true)]
|
||
[SubSection("Cross Bar 5", 0)]
|
||
public class ControllerBar5Config : ControllerBarConfig
|
||
{
|
||
public ControllerBar5Config() => HotbarIndex = 5;
|
||
public new static ControllerBar5Config DefaultConfig() { var c = new ControllerBar5Config(); ApplyDefaults(c, 5); return c; }
|
||
}
|
||
|
||
[Exportable(false)]
|
||
[Section("Controller Hotbars", true)]
|
||
[SubSection("Cross Bar 6", 0)]
|
||
public class ControllerBar6Config : ControllerBarConfig
|
||
{
|
||
public ControllerBar6Config() => HotbarIndex = 6;
|
||
public new static ControllerBar6Config DefaultConfig() { var c = new ControllerBar6Config(); ApplyDefaults(c, 6); return c; }
|
||
}
|
||
|
||
[Exportable(false)]
|
||
[Section("Controller Hotbars", true)]
|
||
[SubSection("Cross Bar 7", 0)]
|
||
public class ControllerBar7Config : ControllerBarConfig
|
||
{
|
||
public ControllerBar7Config() => HotbarIndex = 7;
|
||
public new static ControllerBar7Config DefaultConfig() { var c = new ControllerBar7Config(); ApplyDefaults(c, 7); return c; }
|
||
}
|
||
|
||
[Exportable(false)]
|
||
[Section("Controller Hotbars", true)]
|
||
[SubSection("Cross Bar 8", 0)]
|
||
public class ControllerBar8Config : ControllerBarConfig
|
||
{
|
||
public ControllerBar8Config() => HotbarIndex = 8;
|
||
public new static ControllerBar8Config DefaultConfig() { var c = new ControllerBar8Config(); ApplyDefaults(c, 8); return c; }
|
||
}
|
||
}
|