feat(ui): Campaign OP slice OP8 — Configure Keyboard
Ports retail's Configure Keyboard screen (gmKeyboardUI, LayoutDesc 0x21000009) — its own separate full-screen window, not a fifth Options- panel tab. Retires OP3's INERT contract for the Gameplay tab's Configure Keyboard button (0x10000204). DAT reader (src/AcDream.Core/Input/RetailActionMap.cs): reads the ActionMap singleton (DID 0x26000000, empirically the only one — not 0x27000000 as GetDBOType's Turbine-internal tag would suggest) and both MasterInputMap defaults (0x14000000 "gmDefaultMap"/0x14000002 "DefaultMap"), union-merged per (InputMapId, ActionId) — proven order- independent since the two maps' one shared context (0x5) has disjoint action-id sets. Empirically resolved three lane-D unknowns against the live DAT: the six ActionClass values (1=Movement, 2=Camera, 3=UI, 4=Combat, 5=Emote, 7=CharacterSettings — 6 is genuinely absent), that the six unnamed InputMaps are 100% non-bindable (render nothing, not an unlabeled group), and that the enum-to-DID pairing for the two master maps is inconsequential to the merge result. Identity table (src/AcDream.UI.Abstractions/Input/RetailActionIdentityTable.cs): maps DAT (InputMapId, ActionId) pairs to acdream's InputAction where a live consumer exists (~140 of 306 user-bindable rows — Movement/Camera/ Combat map almost completely; UI/Quickslot/Chat partially; only 5 of 87 Emotes and none of 48 CharacterSettings hotkeys, since acdream has no general emote player or hotkey-to-option-toggle dispatcher yet). Every entry cross-verified by label match AND a DAT-default-vs- KeyBindings.RetailDefaults() byte comparison (RetailActionIdentityRoundTripTests), which caught a real off-by-one in the Quickslot 13-18 block before it shipped and found three genuine pre-existing RetailDefaults() gaps (walk-mode's Shift-echoed chord, ten CameraAlternateControls arrow-key alternates, and the Quickslot Ctrl+N use-vs-select ambiguity) — none introduced by this slice, all documented rather than silently patched. KeyboardConfigController: six ActionClass list boxes built from the DAT, merged with live KeyBindings for mapped rows (rebind applies immediately through the same InputDispatcher every other input path uses) and a new sibling RetailUnmappedKeyBindings store for rows with no InputAction yet. Left-click a key button opens real InputDispatcher modal capture; right-click erases that slot. N-way conflict detection scans every other row plus the live KeyBindings table for acdream-only actions (Ctrl+M mute, debug F-keys) as the non-user-bindable refusal analogue, using retail's own byte-verified "Could not overwrite " string (table 0x23000004). OK/Cancel/Defaults/Revert reuse the OptionPage/IOptionRow verb model via a new ActionKeyMapOptionRow. Persistence is keybinds.json only (D4 — no .keymap file interchange). Five register rows: AP-202 (.keymap interchange narrowing), AP-203 (store-only rows with no live consumer), AP-204 (silent auto-reassign instead of retail's confirm dialog; OK/Cancel ported as left-click not right-click-release). Small supporting additions: UiButton.OnRightClick (additive, no existing behavior changed), InputDispatcher.Bindings getter (the screen's single live-truth read seam), RetailScanCodeMap (DIK scan code <-> Silk.NET Key, keyboard + the one mouse-device row). 19 new tests (6 ActionMap reader conformance incl. live-DAT row-count/ label pins, 1 DAT-vs-RetailDefaults round-trip, 12 controller behavior tests against the committed keyboard_config_21000009.json fixture) — full solution suite 13,147 passed / 4 skipped / 0 failed (baseline 13,128/4/0, zero regressions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ff5776415b
commit
b4edee970f
23 changed files with 35760 additions and 12 deletions
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
|
|
@ -513,6 +514,79 @@ public sealed class BitfieldOptionRow : IOptionRow
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OP slice OP8: one Configure Keyboard row's current/saved/default
|
||||
/// triple — up to 3 <see cref="KeyChord"/> slots (<c>UIOption_ActionKeyMap</c>'s
|
||||
/// <c>m_qclCurrent</c>/<c>m_qclSaved</c>/<c>m_qclDefaults</c>, research doc §5.4).
|
||||
/// Unlike <see cref="BoolOptionRow"/>'s single scalar, <see cref="SetCurrentValue"/>
|
||||
/// here replaces the WHOLE slot list at once — the controller computes the new list
|
||||
/// (one slot rebound via capture, or one slot erased) and calls this with the
|
||||
/// result, mirroring retail's per-slot <c>SetBinding</c>/<c>EraseBinding</c> both
|
||||
/// funnelling through the same <c>UIOption::Apply(1)</c> live-write path.
|
||||
/// </summary>
|
||||
public sealed class ActionKeyMapOptionRow : IOptionRow
|
||||
{
|
||||
private readonly Action<IReadOnlyList<KeyChord>>? _apply;
|
||||
private Action? _notifyPageOptionChanged;
|
||||
private IReadOnlyList<KeyChord> _current;
|
||||
private IReadOnlyList<KeyChord> _saved;
|
||||
private IReadOnlyList<KeyChord> _default;
|
||||
|
||||
public ActionKeyMapOptionRow(
|
||||
IReadOnlyList<KeyChord> initial,
|
||||
IReadOnlyList<KeyChord> defaultValue,
|
||||
Action<IReadOnlyList<KeyChord>>? apply = null)
|
||||
{
|
||||
_current = initial;
|
||||
_saved = initial;
|
||||
_default = defaultValue;
|
||||
_apply = apply;
|
||||
}
|
||||
|
||||
/// <summary>The live slot list — what the row's key buttons currently show.</summary>
|
||||
public IReadOnlyList<KeyChord> Current => _current;
|
||||
|
||||
/// <summary>The committed baseline Revert/Cancel reverts to.</summary>
|
||||
public IReadOnlyList<KeyChord> Saved => _saved;
|
||||
|
||||
/// <summary>The DAT master-map default slot list Reset-to-Defaults restores.</summary>
|
||||
public IReadOnlyList<KeyChord> DefaultValue => _default;
|
||||
|
||||
public bool Changed => !_current.SequenceEqual(_saved);
|
||||
|
||||
/// <summary>Reset-to-Defaults reloads the DAT master maps fresh
|
||||
/// (<c>gmKeyboardUI::RestoreDefaultValues</c> — research doc §5.6) before
|
||||
/// restoring each row, so the default slot list itself can change between
|
||||
/// presses (a fresh DAT read), not just at construction time.</summary>
|
||||
public void SetDefaultValue(IReadOnlyList<KeyChord> value) => _default = value;
|
||||
|
||||
/// <summary>The capture/erase entry point — writes <c>m_current</c> and applies
|
||||
/// it live immediately (retail's per-slot <c>SetBinding</c>/<c>EraseBinding</c>,
|
||||
/// both ending in <c>Apply(1)</c>); does not touch <see cref="Saved"/>.</summary>
|
||||
public void SetCurrentValue(IReadOnlyList<KeyChord> value)
|
||||
{
|
||||
_current = value;
|
||||
_apply?.Invoke(value);
|
||||
_notifyPageOptionChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void AttachPageNotify(Action notify) => _notifyPageOptionChanged = notify;
|
||||
|
||||
public void SaveCurrentValue() => _saved = _current;
|
||||
|
||||
public void RestoreSavedValue()
|
||||
{
|
||||
_current = _saved;
|
||||
_apply?.Invoke(_current);
|
||||
}
|
||||
|
||||
public void RestoreDefaultValue()
|
||||
{
|
||||
_current = _default;
|
||||
_apply?.Invoke(_current);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>OptionPage</c>/<c>PlayerOptionPage</c>: a page's registered-option
|
||||
/// array plus the four verbs (Apply/Reset/Defaults/visibility) with retail's
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue