feat(ui): complete retail quick-slot input
Port global toolbar use/select/create actions, migrate the collapsed Ctrl-number bindings, and route them through a focused retained-UI controller. Preserve shortcut aliases as aliases across inventory and paperdoll drops with retail's neutral/accept/reject drag states, preventing physical item moves such as unwielding an equipped helmet. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
6fcc510d5d
commit
e65119f0c6
24 changed files with 650 additions and 69 deletions
|
|
@ -8,12 +8,10 @@ namespace AcDream.UI.Abstractions.Input;
|
|||
/// debug bindings that have no retail equivalent.
|
||||
///
|
||||
/// <para>
|
||||
/// K.1a defines the enum but only the existing acdream-current chords
|
||||
/// resolve to actions; K.1c flips the bindings table to the full retail
|
||||
/// preset. Several actions in this enum (<c>UseQuickSlot_*</c>,
|
||||
/// <c>Combat*</c>, <c>UseSpellSlot_*</c>) have NO subscribers in K.1 —
|
||||
/// the chord fires the event but nothing acts on it. Phase L panels
|
||||
/// will subscribe.
|
||||
/// K.1a defined the enum and K.1c flipped the bindings table to the full
|
||||
/// retail preset. Runtime controllers subscribe by subsystem; actions whose
|
||||
/// owning panel has not landed yet (for example <c>UseSpellSlot_*</c>) may
|
||||
/// intentionally remain undispatched.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public enum InputAction
|
||||
|
|
@ -142,12 +140,21 @@ public enum InputAction
|
|||
UseQuickSlot_7,
|
||||
UseQuickSlot_8,
|
||||
UseQuickSlot_9,
|
||||
SelectQuickSlot_1,
|
||||
SelectQuickSlot_2,
|
||||
SelectQuickSlot_3,
|
||||
SelectQuickSlot_4,
|
||||
SelectQuickSlot_5,
|
||||
SelectQuickSlot_6,
|
||||
SelectQuickSlot_7,
|
||||
SelectQuickSlot_8,
|
||||
SelectQuickSlot_9,
|
||||
UseQuickSlot_14,
|
||||
UseQuickSlot_15,
|
||||
UseQuickSlot_16,
|
||||
UseQuickSlot_17,
|
||||
UseQuickSlot_18,
|
||||
/// <summary>Drop-target shortcut creation (retail 0 / drag-drop).</summary>
|
||||
/// <summary>Create a toolbar shortcut to the selected object (retail 0).</summary>
|
||||
CreateShortcut,
|
||||
|
||||
// ── Chat ──────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ namespace AcDream.UI.Abstractions.Input;
|
|||
/// </summary>
|
||||
public sealed class KeyBindings
|
||||
{
|
||||
private const int CurrentSchemaVersion = 1;
|
||||
private const int CurrentSchemaVersion = 2;
|
||||
|
||||
private readonly List<Binding> _bindings = new();
|
||||
|
||||
|
|
@ -212,13 +212,18 @@ public sealed class KeyBindings
|
|||
b.Add(new(new KeyChord(Key.Escape, ModifierMask.Shift), InputAction.LOGOUT));
|
||||
|
||||
// ── QuickslotCommands ──────────────────────────────────
|
||||
// Number1..9 → UseQuickSlot_1..9 with bare-mod and Ctrl-mod chords.
|
||||
// Retail gmToolbarUI::ListenToGlobalMessage @ 0x004BE4E0 receives
|
||||
// distinct action-id ranges: bare 1..9 USE slots 0..8, while
|
||||
// Ctrl+1..9 SELECT those slots. The keymap repeats the display name
|
||||
// UseQuickSlot_N for both bindings, so our semantic action layer must
|
||||
// preserve the differing intent explicitly.
|
||||
for (int i = 1; i <= 9; i++)
|
||||
{
|
||||
var k = (Key)((int)Key.Number0 + i); // Number1..Number9
|
||||
var action = (InputAction)((int)InputAction.UseQuickSlot_1 + i - 1);
|
||||
b.Add(new(new KeyChord(k, ModifierMask.None), action));
|
||||
b.Add(new(new KeyChord(k, ModifierMask.Ctrl), action));
|
||||
var useAction = (InputAction)((int)InputAction.UseQuickSlot_1 + i - 1);
|
||||
var selectAction = (InputAction)((int)InputAction.SelectQuickSlot_1 + i - 1);
|
||||
b.Add(new(new KeyChord(k, ModifierMask.None), useAction));
|
||||
b.Add(new(new KeyChord(k, ModifierMask.Ctrl), selectAction));
|
||||
}
|
||||
// Alt+5..9 → UseQuickSlot_14..18.
|
||||
for (int i = 5; i <= 9; i++)
|
||||
|
|
@ -377,9 +382,9 @@ public sealed class KeyBindings
|
|||
using var stream = File.OpenRead(path);
|
||||
var doc = JsonDocument.Parse(stream);
|
||||
var root = doc.RootElement;
|
||||
// version is currently advisory — read so future migrations
|
||||
// can branch on it; the field's presence is non-fatal.
|
||||
_ = root.TryGetProperty("version", out var vEl) ? vEl.GetInt32() : 0;
|
||||
// Missing/older versions are valid and drive narrow migrations;
|
||||
// the field's presence remains non-fatal.
|
||||
int version = root.TryGetProperty("version", out var vEl) ? vEl.GetInt32() : 0;
|
||||
|
||||
var defaults = RetailDefaults();
|
||||
var loaded = new KeyBindings();
|
||||
|
|
@ -413,7 +418,9 @@ public sealed class KeyBindings
|
|||
{
|
||||
device = (byte)dEl.GetInt32();
|
||||
}
|
||||
loaded.Add(new(new KeyChord(silkKey, mods, device), action, activation));
|
||||
var chord = new KeyChord(silkKey, mods, device);
|
||||
action = MigrateLegacyQuickSlotIntent(version, action, chord, activation);
|
||||
loaded.Add(new(chord, action, activation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -481,6 +488,34 @@ public sealed class KeyBindings
|
|||
File.WriteAllText(path, json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schema v1 repeated <c>UseQuickSlot_N</c> for bare and Ctrl chords,
|
||||
/// losing retail's use-vs-select distinction. Migrate only the exact old
|
||||
/// default Ctrl+matching-number shape; arbitrary user rebindings remain
|
||||
/// attached to the action the user chose.
|
||||
/// </summary>
|
||||
private static InputAction MigrateLegacyQuickSlotIntent(
|
||||
int version,
|
||||
InputAction action,
|
||||
KeyChord chord,
|
||||
ActivationType activation)
|
||||
{
|
||||
if (version >= 2
|
||||
|| activation != ActivationType.Press
|
||||
|| chord.Device != 0
|
||||
|| chord.Modifiers != ModifierMask.Ctrl)
|
||||
return action;
|
||||
|
||||
int offset = (int)action - (int)InputAction.UseQuickSlot_1;
|
||||
if ((uint)offset >= 9u)
|
||||
return action;
|
||||
|
||||
var expectedKey = (Key)((int)Key.Number1 + offset);
|
||||
return chord.Key == expectedKey
|
||||
? (InputAction)((int)InputAction.SelectQuickSlot_1 + offset)
|
||||
: action;
|
||||
}
|
||||
|
||||
private static ModifierMask ParseModifiers(JsonElement bindingEl)
|
||||
{
|
||||
if (!bindingEl.TryGetProperty("mod", out var modEl)) return ModifierMask.None;
|
||||
|
|
|
|||
|
|
@ -172,6 +172,11 @@ public sealed class SettingsPanel : IPanel
|
|||
InputAction.UseQuickSlot_1, InputAction.UseQuickSlot_2, InputAction.UseQuickSlot_3,
|
||||
InputAction.UseQuickSlot_4, InputAction.UseQuickSlot_5, InputAction.UseQuickSlot_6,
|
||||
InputAction.UseQuickSlot_7, InputAction.UseQuickSlot_8, InputAction.UseQuickSlot_9,
|
||||
InputAction.SelectQuickSlot_1, InputAction.SelectQuickSlot_2, InputAction.SelectQuickSlot_3,
|
||||
InputAction.SelectQuickSlot_4, InputAction.SelectQuickSlot_5, InputAction.SelectQuickSlot_6,
|
||||
InputAction.SelectQuickSlot_7, InputAction.SelectQuickSlot_8, InputAction.SelectQuickSlot_9,
|
||||
InputAction.UseQuickSlot_14, InputAction.UseQuickSlot_15, InputAction.UseQuickSlot_16,
|
||||
InputAction.UseQuickSlot_17, InputAction.UseQuickSlot_18,
|
||||
InputAction.CreateShortcut,
|
||||
});
|
||||
RenderSection(renderer, "Emotes", new[]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue