fix(ui): OP8 round-2 residuals — inert-row conflict exclusion, DAT-default display, injectivity pin

R1 (code half): store-only rows (MappedAction null) are excluded from the
conflict universe — they never reach the InputDispatcher, so a chord they
display cannot collide; counting them made the ten Camera Alternate
arrow-key defaults trip a false N-way confirm on any arrow rebind. Mapped
cross-context sharing (retail's ConflictingMaps — the combat cluster)
remains deferred as ISSUES #373 with the OP8 gate script now carrying the
explicit do-not-file warning. SHOULD: unmapped rows with no persisted
chords display their DAT defaults (retail shows the arrow keys; blank
read as 'unbound') — display-only, the store is untouched until the row
itself is edited; the independence test updated to pin the new display
semantics while keeping its storage-isolation asserts. Injectivity of
RetailActionIdentityTable is now test-enforced (load-bearing for both M1's
per-row activation capture and M2's de-alias). R2: AP-203 addendum names
the ten same-verb-sibling-live rows and the conflict exclusion.

Full Release suite in this worktree: 13,155 passed / 4 skips / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-11 12:37:45 +02:00
parent b1968ce980
commit f1d502072e
4 changed files with 66 additions and 6 deletions

View file

@ -366,10 +366,19 @@ public sealed class KeyboardConfigController
? (liveBindings[0].Activation, liveBindings[0].Scope)
: (ActivationType.Press, InputScope.Game);
IReadOnlyList<KeyChord> defaults = DatDefaultsToChords(row.DefaultBindings);
IReadOnlyList<KeyChord> storedUnmapped = mapped
? Array.Empty<KeyChord>()
: bindings.CurrentForUnmapped((row.InputMapId, row.ActionId));
// OP8 re-review round 2 (SHOULD-FIX): an unmapped/store-only row with
// no persisted chords displays its DAT DEFAULTS — retail shows the
// authored bindings (the Camera Alternate rows' arrow keys) and a
// blank row misreads as "unbound". Display-only: nothing here feeds
// the InputDispatcher, and the store only gains the defaults if the
// user actually edits the row (the apply closure below).
IReadOnlyList<KeyChord> initial = mapped
? liveBindings.Select(b => b.Chord).ToArray()
: bindings.CurrentForUnmapped((row.InputMapId, row.ActionId));
IReadOnlyList<KeyChord> defaults = DatDefaultsToChords(row.DefaultBindings);
: storedUnmapped.Count > 0 ? storedUnmapped : defaults;
var model = new ActionKeyMapOptionRow(initial, defaults, apply: value =>
{
@ -538,6 +547,15 @@ public sealed class KeyboardConfigController
foreach (RowView other in _rows)
{
if (ReferenceEquals(other, exclude)) continue;
// OP8 re-review round 2 R1: store-only rows (MappedAction null —
// the Camera Alternate scheme, Emote/CharacterSettings hotkeys)
// never reach the InputDispatcher, so a chord they display cannot
// actually collide with anything; counting them made the ten
// arrow-key defaults trip a false N-way confirm on any arrow
// rebind. Retail-mapped cross-context sharing (ConflictingMaps —
// the Insert/Delete/End/PageUp/PageDown combat cluster) remains
// deferred as ISSUES #373; only INERT rows are excluded here.
if (other.MappedAction is null) continue;
if (other.Model.Current.Contains(chord))
rows.Add(other);
}