fix(client): restore retail interaction parity
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:
parent
0c699240e0
commit
f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions
|
|
@ -20,9 +20,8 @@ namespace AcDream.App.Tests.UI.Layout;
|
|||
///
|
||||
/// <para>
|
||||
/// Reworked at the 2026-08-11 combined review (M1/M2/M3/S1/S4): the seam now
|
||||
/// carries <see cref="Binding"/> (chord + activation + scope), the M2 fix means
|
||||
/// only ONE camera InputMap context maps live, and conflicts open a real confirm
|
||||
/// dialog instead of auto-reassigning silently.
|
||||
/// carries <see cref="Binding"/> (chord + activation + scope). Campaign KB gives
|
||||
/// both camera contexts distinct live identities and removes the store-only tier.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class KeyboardConfigControllerTests
|
||||
|
|
@ -55,7 +54,16 @@ public sealed class KeyboardConfigControllerTests
|
|||
uint inputMapId, uint actionId, RetailActionClass cls,
|
||||
uint labelHash = 0, uint tooltipHash = 0,
|
||||
params RetailKeyChord[] defaults) =>
|
||||
new(inputMapId, actionId, cls, labelHash, tooltipHash, defaults);
|
||||
new(
|
||||
inputMapId,
|
||||
actionId,
|
||||
cls,
|
||||
labelHash == 0 ? 0xDE000000u | (actionId & 0x00FFFFFFu) : labelHash,
|
||||
tooltipHash,
|
||||
defaults);
|
||||
|
||||
private static string? ResolveSyntheticString(uint _, uint hash) =>
|
||||
(hash & 0xFF000000u) == 0xDE000000u ? $"Action {hash & 0x00FFFFFFu:X}" : null;
|
||||
|
||||
private sealed class FakeBindings
|
||||
{
|
||||
|
|
@ -72,6 +80,9 @@ public sealed class KeyboardConfigControllerTests
|
|||
public List<uint> InstructionCloses { get; } = new();
|
||||
public uint NextInstructionContext { get; set; } = 7u;
|
||||
public bool WireInstructions { get; set; }
|
||||
public string CurrentKeymapFilename { get; set; } = "acdream.keymap";
|
||||
public Action? PendingLoadCompleted { get; private set; }
|
||||
public Action? PendingSaveCompleted { get; private set; }
|
||||
|
||||
public void Capture(KeyChord? chord)
|
||||
{
|
||||
|
|
@ -103,8 +114,26 @@ public sealed class KeyboardConfigControllerTests
|
|||
BeginCapture: cb => PendingCapture = cb,
|
||||
Save: () => SaveCalls++,
|
||||
Toggle: () => ToggleCalls++,
|
||||
DisplaySystemMessage: msg => Messages.Add(msg),
|
||||
NonBindableRefusalText: "cannot overwrite",
|
||||
ResolveTemplate: (key, variables) => key switch
|
||||
{
|
||||
"ID_ActionKeyMap_ButtonLabel" => variables[DatStringResolver.ComputeHash("LABEL")],
|
||||
"ID_ActionKeyMap_TT_ExistingBinding" =>
|
||||
$"({variables[DatStringResolver.ComputeHash("VALUE")]}) existing binding",
|
||||
"ID_ActionKeyMap_TT_NewBinding" => "new binding",
|
||||
"ID_ActionKeyMap_NonUserBindableBinding" =>
|
||||
$"cannot overwrite {variables[DatStringResolver.ComputeHash("KEY")]}",
|
||||
"ID_ActionKeyMap_OverwriteExistingBinding" =>
|
||||
$"overwrite {variables[DatStringResolver.ComputeHash("KEY")]} "
|
||||
+ variables[DatStringResolver.ComputeHash("ACTION")],
|
||||
"ID_ActionKeyMap_Binding" =>
|
||||
$"{variables[DatStringResolver.ComputeHash("ACTION")]} "
|
||||
+ $"({variables[DatStringResolver.ComputeHash("KEY")]})",
|
||||
"ID_ActionKeyMap_OverwriteExistingBindings" =>
|
||||
$"overwrite {variables[DatStringResolver.ComputeHash("KEY")]}\n"
|
||||
+ variables[DatStringResolver.ComputeHash("BINDINGS")],
|
||||
_ => null,
|
||||
},
|
||||
ShowMessage: msg => Messages.Add(msg),
|
||||
ConfirmOverwrite: (message, onResult) => PendingConfirm = (message, onResult),
|
||||
OpenCaptureInstructions: WireInstructions
|
||||
? label =>
|
||||
|
|
@ -114,11 +143,23 @@ public sealed class KeyboardConfigControllerTests
|
|||
}
|
||||
: null,
|
||||
CloseCaptureInstructions: context => InstructionCloses.Add(context));
|
||||
|
||||
public KeyboardConfigController.Bindings ToProfileBindings() =>
|
||||
ToBindings() with
|
||||
{
|
||||
CurrentKeymapFilename = () => this.CurrentKeymapFilename,
|
||||
OpenLoadKeymap = completed => PendingLoadCompleted = completed,
|
||||
OpenSaveKeymap = completed => PendingSaveCompleted = completed,
|
||||
};
|
||||
}
|
||||
|
||||
private static readonly KeyChord ChordW = new(Silk.NET.Input.Key.W, ModifierMask.None);
|
||||
private static readonly KeyChord ChordUp = new(Silk.NET.Input.Key.Up, ModifierMask.None);
|
||||
private static readonly KeyChord ChordA = new(Silk.NET.Input.Key.A, ModifierMask.None);
|
||||
private static readonly KeyChord LeftMouse = new(
|
||||
InputDispatcher.MouseButtonToKey(Silk.NET.Input.MouseButton.Left),
|
||||
ModifierMask.None,
|
||||
Device: 1);
|
||||
|
||||
[Fact]
|
||||
public void Bind_Succeeds_AndBuildsOneRowPerSnapshotRow()
|
||||
|
|
@ -133,7 +174,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
var fake = new FakeBindings();
|
||||
KeyboardConfigController? controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings());
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings());
|
||||
|
||||
Assert.NotNull(controller);
|
||||
Assert.Equal(3, controller!.Rows.Count);
|
||||
|
|
@ -158,7 +199,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
var fake = new FakeBindings();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
Assert.NotNull(controller);
|
||||
|
||||
UiTabPanel tabHost = Assert.IsType<UiTabPanel>(layout.FindElement(0x1000049Bu));
|
||||
|
|
@ -188,28 +229,28 @@ public sealed class KeyboardConfigControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_MapsKnownActionsAndLeavesUnknownOnesUnmapped()
|
||||
public void Bind_MapsEveryRetailActionRow()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(0x4, 0x29, RetailActionClass.Movement), // -> MovementForward
|
||||
Row(0x10000006, 0x100000A0, RetailActionClass.Emote), // "Bow Deep" -> no InputAction
|
||||
Row(0x10000006, 0x100000A0, RetailActionClass.Emote), // -> EmoteBowDeep
|
||||
});
|
||||
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
var fake = new FakeBindings();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView forward = controller.Rows.Single(r => r.ActionId == 0x29u);
|
||||
Assert.Equal(InputAction.MovementForward, forward.MappedAction);
|
||||
|
||||
KeyboardConfigController.RowView bowDeep = controller.Rows.Single(r => r.ActionId == 0x100000A0u);
|
||||
Assert.Null(bowDeep.MappedAction);
|
||||
Assert.Equal(InputAction.EmoteBowDeep, bowDeep.MappedAction);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_SeedsRowFromLiveBindings_MappedAndUnmapped()
|
||||
public void Bind_SeedsEveryRowFromLiveBindings()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
|
|
@ -223,11 +264,14 @@ public sealed class KeyboardConfigControllerTests
|
|||
new(ChordW, InputAction.MovementForward),
|
||||
new(ChordUp, InputAction.MovementForward),
|
||||
};
|
||||
fake.Unmapped[(0x10000006u, 0x100000A0u)] = new List<KeyChord> { ChordA };
|
||||
fake.Mapped[InputAction.EmoteBowDeep] = new List<Binding>
|
||||
{
|
||||
new(ChordA, InputAction.EmoteBowDeep),
|
||||
};
|
||||
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView forward = controller.Rows.Single(r => r.ActionId == 0x29u);
|
||||
Assert.Equal(new[] { ChordW, ChordUp }, forward.Model.Current);
|
||||
|
|
@ -243,7 +287,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
var fake = new FakeBindings();
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
Assert.NotEmpty(row.KeyButtons);
|
||||
|
|
@ -257,8 +301,8 @@ public sealed class KeyboardConfigControllerTests
|
|||
Assert.Equal(InputAction.MovementForward, written.Action);
|
||||
Binding onlyBinding = Assert.Single(written.Value);
|
||||
Assert.Equal(ChordW, onlyBinding.Chord);
|
||||
// No live binding existed at build time — falls back to the Binding
|
||||
// record's own defaults (Press/Game), same as before M1.
|
||||
// No live binding existed at build time — falls back to the retail
|
||||
// action identity's activation/scope metadata.
|
||||
Assert.Equal(ActivationType.Press, onlyBinding.Activation);
|
||||
Assert.Equal(InputScope.Game, onlyBinding.Scope);
|
||||
Assert.Equal("W", row.KeyButtons[0].Label);
|
||||
|
|
@ -272,7 +316,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
fake.Mapped[InputAction.MovementForward] = new List<Binding> { new(ChordW, InputAction.MovementForward) };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
row.KeyButtons[0].OnClick!.Invoke();
|
||||
|
|
@ -293,7 +337,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
var fake = new FakeBindings { WireInstructions = true, NextInstructionContext = 42u };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
row.KeyButtons[0].OnClick!.Invoke();
|
||||
|
|
@ -314,7 +358,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
var fake = new FakeBindings { WireInstructions = true, NextInstructionContext = 9u };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
row.KeyButtons[0].OnClick!.Invoke();
|
||||
|
|
@ -331,7 +375,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
var fake = new FakeBindings { WireInstructions = true, NextInstructionContext = 0u };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
row.KeyButtons[0].OnClick!.Invoke();
|
||||
|
|
@ -354,7 +398,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
var requests = new List<(uint LayoutId, uint ElementId)>();
|
||||
KeyboardConfigController? controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings(),
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings(),
|
||||
resolveTemplateFont: (layoutId, elementId) =>
|
||||
{
|
||||
requests.Add((layoutId, elementId));
|
||||
|
|
@ -382,7 +426,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
};
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
Assert.Equal(2, row.Model.Current.Count);
|
||||
|
|
@ -400,7 +444,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
var fake = new FakeBindings();
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
Assert.Empty(row.Model.Current);
|
||||
|
|
@ -411,17 +455,17 @@ public sealed class KeyboardConfigControllerTests
|
|||
Assert.Empty(fake.MappedSets);
|
||||
}
|
||||
|
||||
/// <summary>S4 (2026-08-11 review): clicking "Mapping 3" (slot index 2) on a
|
||||
/// row with NO existing bindings must land the captured chord on display
|
||||
/// index 2, not collapse it onto index 0.</summary>
|
||||
/// <summary>Retail SetBinding clamps a requested slot past the dense
|
||||
/// current-list tail to Count. Mapping 3 on an empty row therefore appends
|
||||
/// at Mapping 1.</summary>
|
||||
[Fact]
|
||||
public void KeyButtonClick_OnSparseRow_ThirdSlotLandsOnThirdButton()
|
||||
public void KeyButtonClick_PastDenseTail_AppendsAtFirstAvailableButton()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[] { Row(0x4, 0x29, RetailActionClass.Movement) });
|
||||
var fake = new FakeBindings();
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
Assert.Equal(3, row.KeyButtons.Count);
|
||||
|
|
@ -430,12 +474,10 @@ public sealed class KeyboardConfigControllerTests
|
|||
row.KeyButtons[2].OnClick!.Invoke(); // "Mapping 3"
|
||||
fake.Capture(ChordW);
|
||||
|
||||
Assert.Null(row.KeyButtons[0].Label);
|
||||
Assert.Equal("W", row.KeyButtons[0].Label);
|
||||
Assert.Null(row.KeyButtons[1].Label);
|
||||
Assert.Equal("W", row.KeyButtons[2].Label);
|
||||
Assert.Null(row.KeyButtons[2].Label);
|
||||
|
||||
// The write to the live seam only ever carries the REAL chord — no
|
||||
// default(KeyChord) padding leaks into the persisted Binding list.
|
||||
(InputAction Action, IReadOnlyList<Binding> Value) written = Assert.Single(fake.MappedSets);
|
||||
Binding onlyBinding = Assert.Single(written.Value);
|
||||
Assert.Equal(ChordW, onlyBinding.Chord);
|
||||
|
|
@ -453,7 +495,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
fake.Mapped[InputAction.MovementBackup] = new List<Binding> { new(ChordA, InputAction.MovementBackup) };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView forward = controller.Rows.Single(r => r.ActionId == 0x29u);
|
||||
KeyboardConfigController.RowView backup = controller.Rows.Single(r => r.ActionId == 0x2Au);
|
||||
|
|
@ -463,6 +505,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
|
||||
// M3: nothing is applied yet — a confirm dialog is pending.
|
||||
Assert.NotNull(fake.PendingConfirm);
|
||||
Assert.Equal("overwrite A Action 2A", fake.PendingConfirm?.Message);
|
||||
Assert.DoesNotContain(ChordA, forward.Model.Current);
|
||||
Assert.Contains(ChordA, backup.Model.Current);
|
||||
Assert.Empty(fake.Messages);
|
||||
|
|
@ -473,6 +516,153 @@ public sealed class KeyboardConfigControllerTests
|
|||
Assert.DoesNotContain(ChordA, backup.Model.Current);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_BareShiftConflictWithRetailWalkMode_AlwaysPrompts()
|
||||
{
|
||||
var shift = new KeyChord(
|
||||
Silk.NET.Input.Key.ShiftLeft,
|
||||
ModifierMask.None);
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(0x4, 0x29, RetailActionClass.Movement), // Move forward
|
||||
Row(0x4, 0x32, RetailActionClass.Movement), // Toggle walk/run
|
||||
});
|
||||
var fake = new FakeBindings();
|
||||
fake.Mapped[InputAction.MovementWalkMode] =
|
||||
[new Binding(
|
||||
shift,
|
||||
InputAction.MovementWalkMode,
|
||||
ActivationType.Hold,
|
||||
InputScope.Game)];
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout,
|
||||
snapshot,
|
||||
MakeTemplateResolver(),
|
||||
ResolveSyntheticString,
|
||||
fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView forward = controller.Rows.Single(
|
||||
row => row.ActionId == 0x29u);
|
||||
forward.KeyButtons[0].OnClick!.Invoke();
|
||||
fake.Capture(shift);
|
||||
|
||||
Assert.NotNull(fake.PendingConfirm);
|
||||
Assert.DoesNotContain(shift, forward.Model.Current);
|
||||
Assert.Equal(
|
||||
[shift],
|
||||
controller.Rows.Single(row => row.ActionId == 0x32u).Model.Current);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_ConflictWithMultipleRows_UsesRetailPluralBindingList()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(0x4, 0x29, RetailActionClass.Movement),
|
||||
Row(0x4, 0x2A, RetailActionClass.Movement),
|
||||
Row(0x4, 0x2B, RetailActionClass.Movement),
|
||||
});
|
||||
var fake = new FakeBindings();
|
||||
fake.Mapped[InputAction.MovementBackup] =
|
||||
new List<Binding> { new(ChordA, InputAction.MovementBackup) };
|
||||
fake.Mapped[InputAction.MovementStop] =
|
||||
new List<Binding> { new(ChordA, InputAction.MovementStop) };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
controller.Rows.Single(row => row.ActionId == 0x29u).KeyButtons[0].OnClick!.Invoke();
|
||||
fake.Capture(ChordA);
|
||||
|
||||
Assert.Equal(
|
||||
"overwrite A\nAction 2A (A)\nAction 2B (A)",
|
||||
fake.PendingConfirm?.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Refresh_UsesRetailExistingAndNewBindingTooltipTemplates()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(0x4, 0x29, RetailActionClass.Movement),
|
||||
});
|
||||
var fake = new FakeBindings();
|
||||
fake.Mapped[InputAction.MovementForward] =
|
||||
new List<Binding> { new(ChordW, InputAction.MovementForward) };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = Assert.Single(controller.Rows);
|
||||
Assert.Equal("(W) existing binding", row.KeyButtons[0].TooltipText);
|
||||
Assert.Equal("new binding", row.KeyButtons[1].TooltipText);
|
||||
Assert.Equal("new binding", row.KeyButtons[2].TooltipText);
|
||||
|
||||
row.KeyButtons[0].OnRightClick!.Invoke();
|
||||
Assert.Equal("new binding", row.KeyButtons[0].TooltipText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_ChordAlreadyInAnotherSlotOfSameRow_IsRetailNoOp()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(0x4, 0x29, RetailActionClass.Movement),
|
||||
});
|
||||
var fake = new FakeBindings();
|
||||
fake.Mapped[InputAction.MovementForward] = new List<Binding>
|
||||
{
|
||||
new(ChordW, InputAction.MovementForward),
|
||||
new(ChordUp, InputAction.MovementForward),
|
||||
};
|
||||
// Prove retail's same-row early return happens before the
|
||||
// non-user-bindable conflict check too.
|
||||
fake.Mapped[InputAction.AcdreamToggleAudioMute] = new List<Binding>
|
||||
{
|
||||
new(ChordW, InputAction.AcdreamToggleAudioMute),
|
||||
};
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = Assert.Single(controller.Rows);
|
||||
row.KeyButtons[2].OnClick!.Invoke();
|
||||
fake.Capture(ChordW);
|
||||
|
||||
Assert.Equal(new[] { ChordW, ChordUp }, row.Model.Current);
|
||||
Assert.Null(fake.PendingConfirm);
|
||||
Assert.Empty(fake.Messages);
|
||||
Assert.Empty(fake.MappedSets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_LeftOrRightMouseButton_RemainsArmedUntilSupportedInput()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(0x4, 0x29, RetailActionClass.Movement),
|
||||
});
|
||||
var fake = new FakeBindings { WireInstructions = true };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = Assert.Single(controller.Rows);
|
||||
row.KeyButtons[0].OnClick!.Invoke();
|
||||
fake.Capture(LeftMouse);
|
||||
|
||||
Assert.NotNull(fake.PendingCapture);
|
||||
Assert.Empty(fake.InstructionCloses);
|
||||
Assert.Empty(row.Model.Current);
|
||||
|
||||
fake.Capture(ChordW);
|
||||
|
||||
Assert.Null(fake.PendingCapture);
|
||||
Assert.Equal(new[] { ChordW }, row.Model.Current);
|
||||
Assert.Equal(new[] { 7u }, fake.InstructionCloses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_ConflictWithAnotherRow_DeclineLeavesBothRowsUnchanged()
|
||||
{
|
||||
|
|
@ -486,7 +676,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
fake.Mapped[InputAction.MovementBackup] = new List<Binding> { new(ChordA, InputAction.MovementBackup) };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView forward = controller.Rows.Single(r => r.ActionId == 0x29u);
|
||||
KeyboardConfigController.RowView backup = controller.Rows.Single(r => r.ActionId == 0x2Au);
|
||||
|
|
@ -500,7 +690,71 @@ public sealed class KeyboardConfigControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_ConflictWithNonBindableAcdreamAction_RefusesWithoutADialog()
|
||||
public void Capture_SharedChordAcrossNonConflictingCombatContexts_KeepsBothBindings()
|
||||
{
|
||||
const uint meleeMap = 0x10000003u;
|
||||
const uint missileMap = 0x10000004u;
|
||||
var conflicts = new Dictionary<uint, IReadOnlySet<uint>>
|
||||
{
|
||||
[meleeMap] = new HashSet<uint> { meleeMap },
|
||||
[missileMap] = new HashSet<uint> { missileMap },
|
||||
};
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(meleeMap, 0x1000005Du, RetailActionClass.Combat),
|
||||
Row(missileMap, 0x100000F1u, RetailActionClass.Combat),
|
||||
}, conflicts);
|
||||
var fake = new FakeBindings();
|
||||
fake.Mapped[InputAction.CombatAimLow] =
|
||||
new List<Binding> { new(ChordA, InputAction.CombatAimLow, Scope: InputScope.MissileCombat) };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView melee = controller.Rows.Single(
|
||||
row => row.InputMapId == meleeMap);
|
||||
KeyboardConfigController.RowView missile = controller.Rows.Single(
|
||||
row => row.InputMapId == missileMap);
|
||||
melee.KeyButtons[0].OnClick!.Invoke();
|
||||
fake.Capture(ChordA);
|
||||
|
||||
Assert.Null(fake.PendingConfirm);
|
||||
Assert.Contains(ChordA, melee.Model.Current);
|
||||
Assert.Contains(ChordA, missile.Model.Current);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_SharedChordAcrossDatConflictingContexts_StillPrompts()
|
||||
{
|
||||
const uint movementMap = 0x4u;
|
||||
const uint uiMap = 0x10000009u;
|
||||
var conflicts = new Dictionary<uint, IReadOnlySet<uint>>
|
||||
{
|
||||
[movementMap] = new HashSet<uint> { movementMap, uiMap },
|
||||
};
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(movementMap, 0x29u, RetailActionClass.Movement),
|
||||
Row(uiMap, 0x10000019u, RetailActionClass.Ui),
|
||||
}, conflicts);
|
||||
var fake = new FakeBindings();
|
||||
fake.Mapped[InputAction.ToggleInventoryPanel] =
|
||||
new List<Binding> { new(ChordA, InputAction.ToggleInventoryPanel) };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView movement = controller.Rows.Single(
|
||||
row => row.InputMapId == movementMap);
|
||||
movement.KeyButtons[0].OnClick!.Invoke();
|
||||
fake.Capture(ChordA);
|
||||
|
||||
Assert.NotNull(fake.PendingConfirm);
|
||||
Assert.DoesNotContain(ChordA, movement.Model.Current);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_ConflictWithNonBindableAcdreamAction_ShowsRetailMessageDialog()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[] { Row(0x4, 0x29, RetailActionClass.Movement) });
|
||||
var fake = new FakeBindings();
|
||||
|
|
@ -510,16 +764,17 @@ public sealed class KeyboardConfigControllerTests
|
|||
new List<Binding> { new(muteChord, InputAction.AcdreamToggleAudioMute) };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView forward = controller.Rows.Single();
|
||||
forward.KeyButtons[0].OnClick!.Invoke();
|
||||
fake.Capture(muteChord);
|
||||
|
||||
// S1: refused outright, no confirm dialog offered.
|
||||
// S1: refused outright, with the distinct retail message dialog and no
|
||||
// overwrite-confirmation dialog.
|
||||
Assert.Null(fake.PendingConfirm);
|
||||
Assert.DoesNotContain(muteChord, forward.Model.Current);
|
||||
Assert.Contains("cannot overwrite", fake.Messages);
|
||||
Assert.Contains(fake.Messages, message => message.Contains("cannot overwrite"));
|
||||
Assert.Equal(muteChord, Assert.Single(fake.Mapped[InputAction.AcdreamToggleAudioMute]).Chord);
|
||||
}
|
||||
|
||||
|
|
@ -540,14 +795,14 @@ public sealed class KeyboardConfigControllerTests
|
|||
new List<Binding> { new(sharedChord, InputAction.AcdreamToggleAudioMute) };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView forward = controller.Rows.Single(r => r.ActionId == 0x29u);
|
||||
forward.KeyButtons[0].OnClick!.Invoke();
|
||||
fake.Capture(sharedChord);
|
||||
|
||||
Assert.Null(fake.PendingConfirm);
|
||||
Assert.Contains("cannot overwrite", fake.Messages);
|
||||
Assert.Contains(fake.Messages, message => message.Contains("cannot overwrite"));
|
||||
Assert.DoesNotContain(sharedChord, forward.Model.Current);
|
||||
}
|
||||
|
||||
|
|
@ -558,7 +813,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
var fake = new FakeBindings();
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
row.KeyButtons[0].OnClick!.Invoke();
|
||||
|
|
@ -573,6 +828,102 @@ public sealed class KeyboardConfigControllerTests
|
|||
Assert.Equal(1, fake.ToggleCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFile_ReplacesRowsAndRevertBaseline_AndRefreshesFilename()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(0x4, 0x29, RetailActionClass.Movement),
|
||||
});
|
||||
var fake = new FakeBindings();
|
||||
fake.Mapped[InputAction.MovementForward] =
|
||||
[new Binding(ChordW, InputAction.MovementForward)];
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString,
|
||||
fake.ToProfileBindings())!;
|
||||
|
||||
((UiButton)layout.FindElement(0x10000027u)!).OnClick!.Invoke();
|
||||
Assert.NotNull(fake.PendingLoadCompleted);
|
||||
|
||||
fake.Mapped[InputAction.MovementForward] =
|
||||
[new Binding(ChordUp, InputAction.MovementForward)];
|
||||
fake.CurrentKeymapFilename = "friends.keymap";
|
||||
fake.PendingLoadCompleted!();
|
||||
|
||||
ActionKeyMapOptionRow row = controller.Rows.Single().Model;
|
||||
Assert.Equal(new[] { ChordUp }, row.Current);
|
||||
Assert.Equal(new[] { ChordUp }, row.Saved);
|
||||
Assert.False(row.Changed);
|
||||
UiText filename = (UiText)layout.FindElement(0x10000028u)!;
|
||||
Assert.Equal("friends.keymap", Assert.Single(filename.LinesProvider()).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveAs_RefreshesActiveFilenameOnlyAfterSuccessfulCallback()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(0x4, 0x29, RetailActionClass.Movement),
|
||||
});
|
||||
var fake = new FakeBindings();
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
_ = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString,
|
||||
fake.ToProfileBindings());
|
||||
UiText filename = (UiText)layout.FindElement(0x10000028u)!;
|
||||
|
||||
((UiButton)layout.FindElement(0x10000029u)!).OnClick!.Invoke();
|
||||
Assert.NotNull(fake.PendingSaveCompleted);
|
||||
Assert.Equal("acdream.keymap", Assert.Single(filename.LinesProvider()).Text);
|
||||
|
||||
fake.CurrentKeymapFilename = "alternate.keymap";
|
||||
fake.PendingSaveCompleted!();
|
||||
Assert.Equal("alternate.keymap", Assert.Single(filename.LinesProvider()).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RevertButton_IsEnabledExactlyWhileWorkingMapDiffersFromSavedMap()
|
||||
{
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(0x4, 0x29, RetailActionClass.Movement),
|
||||
});
|
||||
var fake = new FakeBindings();
|
||||
fake.Mapped[InputAction.MovementForward] =
|
||||
new List<Binding> { new(ChordW, InputAction.MovementForward) };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
UiButton revert = (UiButton)layout.FindElement(0x1000002Bu)!;
|
||||
|
||||
// gmKeyboardUI::OnOptionChanged @ 0x004DA890: Ghosted while clean.
|
||||
Assert.False(revert.Enabled);
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
row.KeyButtons[0].OnClick!.Invoke();
|
||||
fake.Capture(ChordUp);
|
||||
Assert.True(controller.Page.Changed);
|
||||
Assert.True(revert.Enabled);
|
||||
|
||||
revert.OnClick!.Invoke();
|
||||
Assert.Equal(new[] { ChordW }, row.Model.Current);
|
||||
Assert.False(controller.Page.Changed);
|
||||
Assert.False(revert.Enabled);
|
||||
|
||||
// Defaults is also a live uncommitted edit when the DAT default does
|
||||
// not equal the saved user map, and therefore re-enables Revert.
|
||||
UiButton defaults = (UiButton)layout.FindElement(0x1000002Au)!;
|
||||
defaults.OnClick!.Invoke();
|
||||
Assert.True(controller.Page.Changed);
|
||||
Assert.True(revert.Enabled);
|
||||
|
||||
UiButton ok = (UiButton)layout.FindElement(0x1000002Cu)!;
|
||||
ok.OnClick!.Invoke();
|
||||
Assert.False(controller.Page.Changed);
|
||||
Assert.False(revert.Enabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancelButton_RevertsUncommittedEditAndToggles()
|
||||
{
|
||||
|
|
@ -581,7 +932,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
fake.Mapped[InputAction.MovementForward] = new List<Binding> { new(ChordW, InputAction.MovementForward) };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
row.KeyButtons[0].OnClick!.Invoke();
|
||||
|
|
@ -607,7 +958,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
fake.Mapped[InputAction.MovementForward] = new List<Binding> { new(ChordUp, InputAction.MovementForward) };
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
Assert.Equal(new[] { ChordUp }, row.Model.Current);
|
||||
|
|
@ -639,7 +990,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
};
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
UiButton defaultsButton = (UiButton)layout.FindElement(0x1000002Au)!;
|
||||
defaultsButton.OnClick!.Invoke();
|
||||
|
|
@ -665,7 +1016,7 @@ public sealed class KeyboardConfigControllerTests
|
|||
};
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView row = controller.Rows.Single();
|
||||
row.KeyButtons[0].OnClick!.Invoke();
|
||||
|
|
@ -695,20 +1046,24 @@ public sealed class KeyboardConfigControllerTests
|
|||
Row(0x6, 0x35, RetailActionClass.Camera, defaults: new[] { new RetailKeyChord(0xCB, 0, 0, 3) }),
|
||||
});
|
||||
var fake = new FakeBindings();
|
||||
fake.Mapped[InputAction.CameraRotateLeft] =
|
||||
[new Binding(ChordA, InputAction.CameraRotateLeft)];
|
||||
fake.Mapped[InputAction.CameraAlternateRotateLeft] =
|
||||
[new Binding(
|
||||
ChordUp,
|
||||
InputAction.CameraAlternateRotateLeft,
|
||||
Scope: InputScope.Camera)];
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView ctx5 = controller.Rows.Single(r => r.InputMapId == 0x5u);
|
||||
KeyboardConfigController.RowView ctx6 = controller.Rows.Single(r => r.InputMapId == 0x6u);
|
||||
|
||||
Assert.Equal(InputAction.CameraRotateLeft, ctx5.MappedAction);
|
||||
Assert.Null(ctx6.MappedAction); // unmapped — no live dual-binding infrastructure (M2)
|
||||
Assert.Equal(InputAction.CameraAlternateRotateLeft, ctx6.MappedAction);
|
||||
|
||||
// Round-2 SHOULD-FIX: an unmapped row with no persisted chords now
|
||||
// DISPLAYS its DAT defaults (retail shows the arrow keys; blank read
|
||||
// as "unbound"). Display-only — storage stays untouched until the
|
||||
// user edits THIS row.
|
||||
// Both rows display their independent live bindings.
|
||||
Assert.NotEmpty(ctx6.Model.Current);
|
||||
var ctx6InitialDisplay = ctx6.Model.Current.ToArray();
|
||||
|
||||
|
|
@ -717,13 +1072,15 @@ public sealed class KeyboardConfigControllerTests
|
|||
fake.Capture(ChordW);
|
||||
Assert.Contains(ChordW, ctx5.Model.Current);
|
||||
Assert.Equal(ctx6InitialDisplay, ctx6.Model.Current); // unchanged by ctx5's edit
|
||||
Assert.Empty(fake.Unmapped); // ctx6's STORE untouched — display seeding writes nothing
|
||||
Assert.Empty(fake.Unmapped);
|
||||
|
||||
ctx6.KeyButtons[0].OnClick!.Invoke();
|
||||
fake.Capture(ChordA);
|
||||
Assert.Contains(ChordA, ctx6.Model.Current);
|
||||
Assert.Contains(ChordW, ctx5.Model.Current); // ctx5 unaffected by ctx6's edit
|
||||
Assert.True(fake.Unmapped.ContainsKey((0x6u, 0x35u)));
|
||||
Assert.Contains(fake.MappedSets, write =>
|
||||
write.Action == InputAction.CameraAlternateRotateLeft
|
||||
&& write.Value.Any(binding => binding.Chord == ChordA));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -756,30 +1113,26 @@ public sealed class KeyboardConfigControllerTests
|
|||
+ $"and (0x{mapId:X}, 0x{actionId:X}) — aliasing reintroduces the M2 twin-row clobber.");
|
||||
seen[action] = (mapId, actionId);
|
||||
}
|
||||
Assert.True(seen.Count > 100, $"sanity: only {seen.Count} mapped actions seen");
|
||||
Assert.Equal(306, seen.Count);
|
||||
}
|
||||
|
||||
// ── AD-78 caption dimming (user-directed, 2026-08-11, gate 2) ───────────
|
||||
|
||||
[Fact]
|
||||
public void UnmappedRows_DimTheirCaption_MappedRowsStayWhite()
|
||||
public void EveryRetailRowCaptionIsEnabled()
|
||||
{
|
||||
// AP-203's store-only set: a row whose RetailActionIdentityTable
|
||||
// lookup fails (MappedAction null -- mostly Emotes/CharacterSettings)
|
||||
// never reaches the InputDispatcher, so its caption dims. Wiring a
|
||||
// future mapping for "Bow Deep" (or any other unmapped row) means
|
||||
// this assertion flips from StoreOnlyCaptionColor to Vector4.One --
|
||||
// a conscious edit, not a silent pass.
|
||||
// Campaign KB removes AP-203's store-only set. Every DAT row now has
|
||||
// a live dispatcher identity and uses the enabled retail caption color.
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(0x4, 0x29, RetailActionClass.Movement), // -> MovementForward (mapped)
|
||||
Row(0x10000006, 0x100000A0, RetailActionClass.Emote), // "Bow Deep" -> unmapped
|
||||
Row(0x10000006, 0x100000A0, RetailActionClass.Emote), // -> EmoteBowDeep
|
||||
});
|
||||
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
var fake = new FakeBindings();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
layout, snapshot, MakeTemplateResolver(), ResolveSyntheticString, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView forward = controller.Rows.Single(r => r.ActionId == 0x29u);
|
||||
Assert.NotNull(forward.MappedAction);
|
||||
|
|
@ -787,9 +1140,9 @@ public sealed class KeyboardConfigControllerTests
|
|||
Assert.Equal(Vector4.One, forwardCaption.DefaultColor);
|
||||
|
||||
KeyboardConfigController.RowView bowDeep = controller.Rows.Single(r => r.ActionId == 0x100000A0u);
|
||||
Assert.Null(bowDeep.MappedAction);
|
||||
Assert.Equal(InputAction.EmoteBowDeep, bowDeep.MappedAction);
|
||||
UiText bowDeepCaption = RowCaption(bowDeep);
|
||||
Assert.Equal(UiRenderContext.StoreOnlyCaptionColor, bowDeepCaption.DefaultColor);
|
||||
Assert.Equal(Vector4.One, bowDeepCaption.DefaultColor);
|
||||
}
|
||||
|
||||
/// <summary>The row's synthesized caption (composed beside the authored key
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue