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
File diff suppressed because one or more lines are too long
|
|
@ -424,5 +424,5 @@ before anything builds on them.
|
|||
| OP5 | CODE-COMPLETE, gate READY | `e71e5a96` (AP-195 retired) → fixes `6d0b0f92` → residuals `67b0815c` | combined APPROVE-WITH-FIXES (`2026-08-11-op5-review.md`) → re-check CLOSED (`2026-08-11-op5-recheck.md`) → coordinator drag residuals landed | connected gate OWED (script §OP5) |
|
||||
| OP6 | CODE-COMPLETE, gate READY | `f5ac1742` (REJECTED) → rework `472525b9` → doc residuals (coordinator) | REJECT (`2026-08-11-op6-review.md`) → re-review CLOSED, all six caption sites byte-decoded (`2026-08-11-op6-rereview.md`) | connected gate OWED (script §OP6) |
|
||||
| OP7 | CLOSED | `09cb548a` → fixes in `7b60e71b` (shared commit, see its message) | combined-lens APPROVE-WITH-FIXES (`2026-08-11-op7-review.md`); all nine findings closed | live bot-vs-ACE gate PASSED 2026-08-11 (coordinator; evidence in script §OP7) |
|
||||
| OP8 | — | | | |
|
||||
| OP8 | CODE-COMPLETE, gate READY | (this branch's commit) | not yet reviewed | connected gate OWED (script §OP8) |
|
||||
| OP9 | — | | | |
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
# Campaign OP connected-gate test script
|
||||
|
||||
**Status:** OP3, OP4, OP5, OP6, and OP7 sections. Later slices (OP8) append
|
||||
their own sections here as they land; the campaign's OP9 closeout gate is
|
||||
this document complete plus every slice code-complete.
|
||||
**Status:** OP3, OP4, OP5, OP6, OP7, and OP8 sections. The campaign's OP9
|
||||
closeout gate is this document complete plus every slice code-complete.
|
||||
|
||||
This document is the script the user runs against the live connected
|
||||
client (`ACDREAM_LIVE=1` against the local ACE server) to accept each
|
||||
|
|
@ -851,3 +850,149 @@ Three live runs against local ACE (`127.0.0.1:9000`, `+Acdream`,
|
|||
unmapped `[weenie-error] code=0x051D`, and the K4 resource envelope's
|
||||
`handle-count` violation (the `k4-linux-30-session` Linux profile
|
||||
evaluated on Windows).
|
||||
|
||||
---
|
||||
|
||||
## OP8 — Configure Keyboard
|
||||
|
||||
The screen is retail's own separate full-screen window (`gmKeyboardUI`,
|
||||
LayoutDesc `0x21000009`), NOT a fifth tab of the Options panel. It opens
|
||||
from the Gameplay tab's Configure Keyboard button — **the OP3 INERT
|
||||
contract for that button is retired as of this slice**; OP3's own script
|
||||
line calling it INERT no longer applies.
|
||||
|
||||
### Opening the screen
|
||||
|
||||
1. **Open the Options panel (F11), Gameplay tab, click "Configure
|
||||
Keyboard."** A separate full-screen window opens (not layered inside
|
||||
the Options panel) showing six tabs across the top: Movement, Camera,
|
||||
Combat, UI, CharacterSettings, Emote — Movement is the default/first
|
||||
tab. Each tab lists grouped rows: a bold-ish header naming the
|
||||
sub-category (e.g. "Movement Commands"), then one row per action —
|
||||
an action label on the left, up to three key-binding buttons to its
|
||||
right (retail's "Mapping 1/2/3" columns) showing the currently bound
|
||||
key(s) (e.g. "W", "Up").
|
||||
2. **Not every group has a visible header/rows.** Debug/dialog/system
|
||||
InputMaps (DialogBoxes, DebugConsole, ProfilerUI, UIDebugger,
|
||||
DebugCommands, and six further unnamed contexts) are 100% non-user-
|
||||
bindable in the shipped DAT and correctly show NOTHING — this is not
|
||||
a bug, it is retail's own shipped data (byte-verified against the
|
||||
live DAT, see `RetailActionMap.cs`'s class doc).
|
||||
3. **The bottom-row buttons:** Load File…, a current-keymap-name label,
|
||||
Save As…, Defaults, Revert, OK, Cancel. Load File / Save As are
|
||||
**INERT** (D4 — no `.keymap` file interchange, AP-202) — clicking
|
||||
them does nothing; this is correct, contracted behavior.
|
||||
|
||||
### Rebind a movement key live
|
||||
|
||||
4. **On the Movement tab, find "Move Forward"** (should show two bound
|
||||
keys, "W" and "Up"). Click the SECOND key button (currently "Up").
|
||||
The button should visually indicate it is listening for input
|
||||
(retail's own capture-prompt text is not wired to a tooltip in this
|
||||
port — a simple pressed/active state is enough to confirm capture
|
||||
started).
|
||||
5. **Press a different key**, e.g. `U`. The button should immediately
|
||||
update to show "U".
|
||||
6. **Move your character forward using W and U** (both should now work
|
||||
— this is the live-effect proof: the rebind reached the SAME
|
||||
`InputDispatcher`/`KeyBindings` every other input path uses, not a
|
||||
screen-local shadow copy). The original "Up" arrow should no longer
|
||||
move the character forward.
|
||||
7. **Right-click the "U" key button** you just set. It should clear
|
||||
back to blank/empty — right-click erases just that one slot
|
||||
(`EraseBinding`), distinct from a hypothetical "Clear all" (there is
|
||||
no Clear-all button on this screen — the shipped 2013 row template
|
||||
authors none; a real, DAT-verified fact, not a limitation of this
|
||||
port).
|
||||
|
||||
### Conflict on a taken chord
|
||||
|
||||
8. **Still on Movement, click "Move Backward"'s first key button**
|
||||
(currently "X"), then press **W** — the SAME key you just confirmed
|
||||
is bound to "Move Forward."
|
||||
9. **Expect**: "Move Backward" takes W, and "Move Forward" silently
|
||||
loses its W slot (down to just "U" from step 5) — the SAME outcome
|
||||
you'd get after confirming retail's own "reassign?" dialog, except
|
||||
this port applies it immediately without asking first (AP-204,
|
||||
register row — a deliberate scope narrowing for this slice; retail
|
||||
shows a confirm dialog before reassigning). No error, no crash, both
|
||||
rows' key-button labels update to reflect the swap.
|
||||
|
||||
### Non-bindable refusal
|
||||
|
||||
10. **Click any key button, then press Ctrl+M** (acdream's own
|
||||
debug-only mute toggle — retail has no equivalent, so no DAT row on
|
||||
this screen owns it). **Expect**: the rebind is REFUSED — the key
|
||||
button you clicked keeps its previous value, and a system message
|
||||
appears reading "Could not overwrite " (retail's own byte-verified
|
||||
`ID_KeyMapCantOverwriteReadOnlyKeymap_Label` string, table
|
||||
`0x23000004` — it may read as an odd sentence fragment on its own;
|
||||
that is the literal stored string, not a truncation bug in this
|
||||
port). Confirm Ctrl+M still mutes/unmutes audio afterward — the
|
||||
acdream-only binding was NOT touched.
|
||||
|
||||
### Erase, then Reset to Defaults
|
||||
|
||||
11. **Erase a couple more bindings** via right-click (any tab).
|
||||
12. **Click Defaults.** Every row on EVERY tab should snap back to its
|
||||
retail-default key(s) LIVE — including the ones you just erased,
|
||||
the "Move Forward"/"Move Backward" pair from steps 5-9 (both back
|
||||
to their original W/Up and X/Down), and the Ctrl+M row from step 10
|
||||
is untouched (it has no DAT row, so Defaults cannot and does not
|
||||
touch it). The OK/Cancel/Revert buttons should now read as
|
||||
"changed" (retail never gates Defaults itself, but the page as a
|
||||
whole is dirty after it runs) — confirm by tabbing away and back:
|
||||
edits should still be there (Defaults applies live without
|
||||
committing, exactly like a manual edit would).
|
||||
13. **Click Revert** (not Cancel) with SOME rows still showing your
|
||||
Defaults-restored values. Expect every changed row to revert to
|
||||
whatever was bound when you last clicked OK (or, if you haven't
|
||||
clicked OK yet this session, back to what was loaded from
|
||||
`keybinds.json` at startup) — Revert and Cancel run the identical
|
||||
verb (`RestoreSavedValues`); Revert just doesn't also close the
|
||||
window.
|
||||
|
||||
### Persistence across relaunch
|
||||
|
||||
14. **Rebind one distinctive key** (e.g. change "Jump" from Space to
|
||||
some other free key) and **click OK.** The window closes.
|
||||
15. **Close acdream gracefully and relaunch it** (see CLAUDE.md's
|
||||
logout-before-reconnect discipline — wait for the graceful-close
|
||||
session-clear window before reconnecting). Open Configure Keyboard
|
||||
again: the rebind from step 14 should still be there — proof it
|
||||
persisted to `keybinds.json` on disk, not just the in-memory
|
||||
dispatcher.
|
||||
16. **Open a plain text editor on `%LOCALAPPDATA%\acdream\keybinds.json`**
|
||||
(or the platform-portable equivalent) and confirm the rebound
|
||||
action shows the new key. A sibling `keybinds-unmapped.json` should
|
||||
also exist if you rebound anything on the CharacterSettings or
|
||||
Emote tabs during this session (AP-203's store-only rows — e.g. try
|
||||
rebinding one "Bow Deep"-style Emote row and confirm it shows up in
|
||||
THAT file, not `keybinds.json`).
|
||||
|
||||
### Cancel discards uncommitted edits
|
||||
|
||||
17. **Rebind another key WITHOUT clicking OK**, then click **Cancel.**
|
||||
The window closes; reopen it — the rebind from this step should be
|
||||
GONE (reverted to the last-committed/loaded state), matching step
|
||||
13's Revert behavior plus the window closing.
|
||||
|
||||
### What to report
|
||||
|
||||
- Any row that shows a DIFFERENT key than what `keybinds.json` /
|
||||
`RetailDefaults()` says it should (a sign the DAT-vs-InputAction
|
||||
identity table mis-mapped a row — see
|
||||
`RetailActionIdentityRoundTripTests` for the automated half of this
|
||||
check).
|
||||
- Any tab/header that renders EMPTY where it should show rows, or vice
|
||||
versa (the page-scoped element-lookup trap this slice's controller
|
||||
explicitly guards against — a regression here would mean one page's
|
||||
rows leaked into another, or the six reused element ids resolved to
|
||||
the wrong page's instance).
|
||||
- Whether the silent auto-reassign (step 9) or the refusal message's
|
||||
odd phrasing (step 10) feels wrong enough in practice to warrant
|
||||
building the real confirm-dialog integration AP-204 defers.
|
||||
- Any row whose Emote/CharacterSettings binding visibly DOES something
|
||||
in-game despite AP-203 saying it shouldn't (would mean acdream grew a
|
||||
consumer for it since this table was written, and the identity table
|
||||
should be updated to route it live).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue