fix(client): restore retail interaction parity
All checks were successful
CI / linux-portable (push) Successful in 3m27s
CI / windows-gate (push) Successful in 6m42s
CI / release (push) Successful in 2m12s

Harden keyboard and camera routing, inventory and vendor interactions, chat/emotes, relog portal flow, and paperdoll rendering. Add retail research, connected gate coverage, and release-gate validation.
This commit is contained in:
Erik 2026-08-26 20:45:11 +02:00
parent 0c699240e0
commit f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions

View file

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using AcDream.Content;
using AcDream.Core.Input;
using AcDream.Runtime.Gameplay;
using AcDream.UI.Abstractions.Input;
using DatReaderWriter;
using Xunit;
@ -9,84 +10,13 @@ using Xunit;
namespace AcDream.Core.Tests.Input;
/// <summary>
/// Campaign OP slice OP8: pins <see cref="RetailActionIdentityTable"/>'s agreement
/// with <see cref="KeyBindings.RetailDefaults"/> — for every <see cref="InputAction"/>
/// this slice's table resolves, the UNION of DAT default bindings across every DAT
/// row mapped to that action must equal <c>KeyBindings.RetailDefaults()</c>'s chord
/// set for it. Per the slice contract: "investigate + report any disagreement rather
/// than silently preferring one." Skips cleanly when the installed dats are
/// unavailable (CI), matching every other live-DAT conformance test in this project.
///
/// <para>
/// <b>Two real, byte-verified disagreements survive after the mechanism fixes</b>
/// (2026-08-11 investigation, updated at the M2 rework — none are bugs in this
/// slice's table; both are PRE-EXISTING <see cref="KeyBindings.RetailDefaults"/>
/// gaps/design choices this slice does not touch, listed in
/// <see cref="KnownRetailDefaultsDisagreements"/> with citations). A THIRD
/// disagreement — ten CameraAlternateControls (InputMap 0x6) actions — was RETIRED
/// at the M2 rework: <see cref="RetailActionIdentityTable"/> no longer maps InputMap
/// 0x6 to any <see cref="InputAction"/> at all (the aliasing that produced two
/// independent rows fighting over one live target — M2, 2026-08-11 review), so this
/// test never sees a ctx-0x6 row and the ctx-0x5-only union now matches
/// <c>RetailDefaults()</c> exactly for all twelve Camera actions with no allowlist
/// entry needed:
/// </para>
/// <list type="number">
/// <item><description><b>MovementWalkMode.</b> The DAT's raw <c>QualifiedControl.Modifier</c>
/// for the Shift-key binding is 0 (the key itself IS Shift — there is no separate
/// "modifier" to report when the primary key and the modifier are the same physical
/// key). <c>RetailDefaults()</c> deliberately encodes <c>Modifiers=Shift</c> anyway —
/// its own comment (K-fix1, 2026-04-26) explains the OS echoes
/// <c>CurrentModifiers=Shift</c> alongside a Shift key-DOWN event, so the chord must
/// carry the flag to match at dispatch time. Not a disagreement to fix; a raw-DAT
/// artifact this slice's reader faithfully reproduces.</description></item>
/// <item><description><b>Quickslot 1-9's Ctrl+N chord (and its SelectQuickSlot_1-9
/// counterpart).</b> The DAT's own default
/// master map binds Ctrl+1..9 to the SAME action id as bare 1..9 ("Quickslot N" —
/// <c>UseQuickSlot_N</c>), NOT to the separate "Select Quickslot N" action id
/// (<c>SelectQuickSlot_N</c>, DAT action ids <c>0x1000004E-56</c>) — those carry NO
/// default binding at all in the shipped DAT. <c>RetailDefaults()</c>'s own comment
/// (citing <c>gmToolbarUI::ListenToGlobalMessage @0x004BE4E0</c>) asserts retail's
/// CLIENT reinterprets Ctrl+N contextually as Select — a runtime behavior this raw
/// keymap-default probe cannot see (it reads bound ACTIONS, not the dispatch
/// function's own modifier branching). Both readings are independently retail-
/// sourced; reconciling them needs the decompiled dispatch function, out of scope
/// here. Reported, not silently resolved either way.</description></item>
/// </list>
/// Campaign KB's installed-DAT contract: all 306 user-bindable ActionMap rows
/// have distinct live identities and their default chord sets match the two
/// retail MasterInputMaps exactly. Missing, aliased, or guessed rows fail here.
/// </summary>
[Trait("Lane", "InstalledDat")]
public sealed class RetailActionIdentityRoundTripTests
{
/// <summary>Actions with a citation-backed, pre-existing reason their DAT-union
/// default set legitimately differs from <see cref="KeyBindings.RetailDefaults"/>
/// — see class doc. Every other mapped action must match exactly.</summary>
private static readonly HashSet<InputAction> KnownRetailDefaultsDisagreements = new()
{
InputAction.MovementWalkMode,
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,
// Same Use-vs-Select ambiguity as the bare-numeral block above: the DAT's
// own "Select Quickslot N" action ids carry NO default binding at all —
// RetailDefaults()'s Ctrl+N->Select mapping rests on the decompiled
// dispatch function's runtime modifier check, not the raw keymap default.
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,
};
[Fact]
public void MappedActions_DatUnionDefaultBindings_MatchRetailDefaults()
{
@ -98,13 +28,44 @@ public sealed class RetailActionIdentityRoundTripTests
RetailActionMapSnapshot? snapshot = RetailActionMapReader.Read(source);
Assert.NotNull(snapshot);
Assert.Equal(306, snapshot!.Rows.Count);
var unresolvedRows = snapshot.Rows
.Where(row => !RetailActionIdentityTable.TryResolve(
row.InputMapId,
row.ActionId,
out _))
.Select(row => $"0x{row.InputMapId:X8}/0x{row.ActionId:X8}")
.ToArray();
Assert.Empty(unresolvedRows);
Assert.Equal(306, RetailActionIdentityTable.Map.Count);
Assert.Equal(306, RetailActionIdentityTable.Map.Values.Distinct().Count());
Assert.Equal(306, RetailActionIdentityTable.ReverseMap.Count);
var optionIds = new HashSet<uint>();
foreach (RetailActionMapRow row in snapshot.Rows.Where(
static row => row.InputMapId == 0x10000008u))
{
Assert.True(RetailActionIdentityTable.TryResolve(
row.InputMapId,
row.ActionId,
out InputAction action));
Assert.True(
RetailActionIdentityTable.TryGetCharacterOptionId(
action,
out uint optionId),
$"CharacterSettings row 0x{row.ActionId:X8} has no PlayerOption id");
Assert.True(CharacterOptionTable.TryGet(optionId, out _));
Assert.True(optionIds.Add(optionId), $"duplicate PlayerOption id 0x{optionId:X2}");
}
Assert.Equal(48, optionIds.Count);
KeyBindings retailDefaults = KeyBindings.RetailDefaults();
// Aggregate DAT default chords by resolved InputAction — a single action can
// be reached by more than one DAT row (e.g. the Camera/CameraAlternate pair).
var datChordsByAction = new Dictionary<InputAction, HashSet<KeyChord>>();
var unresolvedScanCodes = new List<string>();
foreach (RetailActionMapRow row in snapshot!.Rows)
foreach (RetailActionMapRow row in snapshot.Rows)
{
if (!RetailActionIdentityTable.TryResolve(row.InputMapId, row.ActionId, out InputAction action))
continue;
@ -125,15 +86,12 @@ public sealed class RetailActionIdentityRoundTripTests
}
}
Assert.True(datChordsByAction.Count > 100,
$"expected >100 mapped actions, got {datChordsByAction.Count}");
Assert.Equal(306, datChordsByAction.Count);
Assert.Empty(unresolvedScanCodes);
var mismatches = new List<string>();
foreach ((InputAction action, HashSet<KeyChord> datChords) in datChordsByAction)
{
if (KnownRetailDefaultsDisagreements.Contains(action)) continue;
var acdreamChords = retailDefaults.ForAction(action).Select(b => b.Chord).ToHashSet();
if (!datChords.SetEquals(acdreamChords))
{
@ -144,8 +102,7 @@ public sealed class RetailActionIdentityRoundTripTests
}
Assert.True(mismatches.Count == 0,
$"{mismatches.Count} unexpected DAT-vs-RetailDefaults() disagreements "
+ "(not in the documented KnownRetailDefaultsDisagreements allowlist):\n"
$"{mismatches.Count} DAT-vs-RetailDefaults() disagreements:\n"
+ string.Join("\n", mismatches));
}
}