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:
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Game.ClientState.GamePad;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Plugin.Services;
|
||||
using HSUI.Config;
|
||||
using HSUI.Interface.GeneralElements;
|
||||
|
||||
namespace HSUI.Helpers
|
||||
{
|
||||
/// <summary>Polls controller bar keybinds and executes slots when keys/buttons are pressed. Supports combinations (e.g. L2+South).</summary>
|
||||
public static class ControllerBarKeybindExecutor
|
||||
{
|
||||
private static readonly Dictionary<string, bool> _triggerWasDown = new Dictionary<string, bool>();
|
||||
|
||||
public static void Process(IFramework framework)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ControllerHotbarsConfig.GetEffectiveControllerHotbarsEnabled())
|
||||
return;
|
||||
|
||||
var keybindsConfig = ConfigurationManager.Instance?.GetConfigObject<ControllerBarKeybindsConfig>();
|
||||
if (keybindsConfig == null)
|
||||
return;
|
||||
|
||||
if (ActionBarsManager.Instance == null)
|
||||
return;
|
||||
|
||||
for (int bar = 1; bar <= ControllerBarKeybindsConfig.Bars; bar++)
|
||||
{
|
||||
for (int slot = 0; slot < ControllerBarKeybindsConfig.SlotsPerBar; slot++)
|
||||
{
|
||||
string binding = keybindsConfig.GetKeybind(bar, slot);
|
||||
if (string.IsNullOrEmpty(binding)) continue;
|
||||
|
||||
bool triggered = IsKeybindTriggered(binding);
|
||||
if (triggered)
|
||||
ActionBarsManager.Instance.ExecuteControllerSlot(bar, slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger?.Warning($"[HSUI ControllerBarKeybinds] Process: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>True when all modifiers are held and the trigger key/button was just pressed (edge).</summary>
|
||||
private static bool IsKeybindTriggered(string binding)
|
||||
{
|
||||
if (string.IsNullOrEmpty(binding)) return false;
|
||||
|
||||
string[] parts = binding.Split('+');
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
parts[i] = parts[i].Trim();
|
||||
|
||||
if (parts.Length == 0) return false;
|
||||
if (parts.Length == 1)
|
||||
{
|
||||
bool down = IsSingleKeybindDown(parts[0]);
|
||||
if (!_triggerWasDown.TryGetValue(binding, out bool wasDown)) wasDown = false;
|
||||
_triggerWasDown[binding] = down;
|
||||
return down && !wasDown;
|
||||
}
|
||||
|
||||
string triggerPart = parts[parts.Length - 1];
|
||||
for (int i = 0; i < parts.Length - 1; i++)
|
||||
{
|
||||
if (!IsSingleKeybindDown(parts[i]))
|
||||
return false;
|
||||
}
|
||||
bool triggerDown = IsSingleKeybindDown(triggerPart);
|
||||
if (!_triggerWasDown.TryGetValue(binding, out bool triggerWasDown))
|
||||
triggerWasDown = false;
|
||||
_triggerWasDown[binding] = triggerDown;
|
||||
return triggerDown && !triggerWasDown;
|
||||
}
|
||||
|
||||
private static bool IsSingleKeybindDown(string part)
|
||||
{
|
||||
if (string.IsNullOrEmpty(part)) return false;
|
||||
|
||||
if (part.StartsWith("Key:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string vkStr = part.Substring(4).Trim();
|
||||
if (Plugin.KeyState == null) return false;
|
||||
if (Enum.TryParse<VirtualKey>(vkStr, true, out var vk)
|
||||
&& Plugin.KeyState.IsVirtualKeyValid((int)vk))
|
||||
return Plugin.KeyState[vk];
|
||||
return false;
|
||||
}
|
||||
|
||||
if (part.StartsWith("Pad:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string padStr = part.Substring(4).Trim();
|
||||
if (Plugin.GamepadState == null) return false;
|
||||
if (Enum.TryParse<GamepadButtons>(padStr, true, out var btn))
|
||||
return Plugin.GamepadState.Raw(btn) > 0.5f;
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user