feat(input): #23 Phase K.1c - retail-faithful keymap cutover + JSON persistence (muscle memory change)
Single bisectable commit where the user-visible keyboard layout flips from acdream-current (W/S/A/D/Z/X) to canonical AC retail (W/X/A/D/Z/C). The InputDispatcher abstraction landed in K.1a, existing handlers cut over in K.1b, and now KeyBindings.RetailDefaults() returns the byte-precise retail preset matching docs/research/named-retail/retail-default.keymap.txt. Movement (matches AC1 muscle memory): - W/Up = MovementForward (run by default) - X/Down = MovementBackup - A/Left = MovementTurnLeft - D/Right = MovementTurnRight - Z = MovementStrafeLeft - C = MovementStrafeRight - Alt+A / Alt+Left = MovementStrafeLeft (Alt-flips-turn) - Alt+D / Alt+Right = MovementStrafeRight - LShift (Hold) = MovementWalkMode (default = run; held = walk) - Q = MovementRunLock (autorun toggle) - S = MovementStop (sets Ready stance / idle) - Space = MovementJump (hold to charge) - Y = Ready, G = Sitting, H = Crouch, B = Sleeping (postures) Selection / targeting (18 bindings on punctuation cluster): - F = SelectionPickUp, T = SelectionSplitStack, P = PreviousSelection - Backspace/Minus/Equals = closest/prev/next CompassItem - Backslash/[/] = closest/prev/next Item - Apostrophe/L/Semicolon = closest/prev/next Monster - Home = LastAttacker - Slash/Comma/Period = closest/prev/next Player - N/M = prev/next Fellow - E = SelectionExamine - R = UseSelected UI: - F1 = ToggleHelp; Shift+Ctrl+F1 = TogglePluginManager - F3 = Allegiance, F4 = Fellowship, F5 = Spellbook, F6 = SpellComponents - F8 = Attributes, F9 = Skills, F10 = World, F11 = Options (lights up the Settings panel in K.3), F12 = Inventory - Alt+1/2/3/4 = ToggleFloatingChatWindow1/2/3/4 - Esc = EscapeKey, Shift+Esc = LOGOUT - Numpad * = CaptureScreenshot Hotbar / spellbook: - 1-9 = UseQuickSlot_1..9 (hotbar) AND UseSpellSlot_1..9 (in MagicCombat scope - dormant until Phase L) - Ctrl+1-9 = UseQuickSlot_1..9 (duplicate) - Alt+5-9 = UseQuickSlot_14..18 (second bar) - 0 / Ctrl+0 = CreateShortcut Chat: - Tab = ToggleChatEntry (focus chat input; subscriber stub-TODO in K.2) - Return = EnterChatMode (send) Combat (mode-dependent, dormant - Phase L lights up): - Grave (`) = CombatToggleCombat - Insert/PgUp/Delete/End/PgDn = melee power+attack-level OR missile accuracy+aim-level OR magic spell-tab nav + cast (resolved by scope at runtime once CombatState.CurrentMode lands). - Ctrl+Insert/PgUp/Delete/PgDn = first/last spell tab + first/last spell Emotes: U = Cry, I = Laugh, J = Wave, O = Cheer, K = PointState Camera (numpad cluster + F2): - F2 / Numpad/ = CameraActivateAlternateMode - Numpad 4/6/8/2 = rotate left/right/up/down - Numpad - / + = move toward / away - Numpad 0 = ViewDefault, Numpad . = FirstPerson - Numpad 5 = LookDown, Numpad Enter = MapMode Scroll: - Mouse wheel handled by dispatcher OnScroll path - Ctrl+Up / Ctrl+Down = ScrollUp / ScrollDown Acdream debug actions relocated from F-keys to Ctrl+F-keys to avoid retail conflicts: - Ctrl+F1 = AcdreamToggleDebugPanel - Ctrl+F2 = AcdreamToggleCollisionWires - Ctrl+F3 = AcdreamDumpNearby - Ctrl+F7 = AcdreamCycleTimeOfDay - Ctrl+F8 / Ctrl+F9 = AcdreamSensitivityDown / Up - Ctrl+F10 = AcdreamCycleWeather AcdreamToggleFlyMode + AcdreamTogglePlayerMode have NO keyboard binding in retail-default. K.2 adds a DebugPanel button for fly toggle and auto-enter player mode at login. Total: 149 bindings. JSON load/save: - KeyBindings.LoadOrDefault(path): merge-over-defaults migration. Missing actions get default bindings; unknown actions in user file are skipped (preserves user customizations across action enum additions). Corrupt file warns + returns RetailDefaults without overwriting (don't blow away user's file silently). - KeyBindings.SaveToFile(path): writes with schema version=1, alpha- sorted action names, alpha-sorted modifier keys for stable diffs. - KeyBindings.DefaultPath() = %LOCALAPPDATA%/acdream/keybinds.json. GameWindow startup: - Replaces KeyBindings.AcdreamCurrentDefaults() call with KeyBindings.LoadOrDefault(KeyBindings.DefaultPath()) via a small LoadStartupKeyBindings() helper. - Logs "keybinds: loaded N bindings from <path>" so launch.log shows the source of truth at session start. Three deviations from plan: 1. LoadStartupKeyBindings() helper instead of inline initializer (field initializer can't call methods directly). 2. ToggleChatEntry subscriber is a no-op stub with TODO K.2 comment (ChatPanel doesn't expose FocusInput() yet; will add in K.2). 3. AcdreamRmbOrbitHold removed from RetailDefaults() to avoid double-binding RMB (SelectRight + RmbOrbitHold on the same chord would fire both subscribers). Chase-camera orbit will be replaced by MMB-hold mouse-look in K.2 - retail's CameraInstantMouseLook. 28 new tests: - KeyBindingsRetailTests: 19 cases pinning every retail mapping (W/X movement, Z/C strafe, Tab=ToggleChatEntry, Shift+Esc=LOGOUT, Shift+Ctrl+F1=TogglePluginManager, MovementWalkMode=Hold, Acdream debug on Ctrl+F*, hotbar number-row variants, etc). - KeyBindingsJsonTests: 9 cases (round-trip; missing file → defaults; corrupt → defaults + no-overwrite; merge-over-defaults; legacy version=0 parsing; Hold-activation preservation; unknown- action skip; DefaultPath shape). Solution total: 1162 green (243 Core.Net + 254 UI + 665 Core), 0 warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
256e9624bd
commit
da189103b8
5 changed files with 847 additions and 43 deletions
|
|
@ -0,0 +1,234 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
using Silk.NET.Input;
|
||||
|
||||
namespace AcDream.UI.Abstractions.Tests.Input;
|
||||
|
||||
/// <summary>
|
||||
/// K.1c: <see cref="KeyBindings.LoadOrDefault"/> /
|
||||
/// <see cref="KeyBindings.SaveToFile"/> round-trip + version migration.
|
||||
/// Pins schema-stable behavior so future K.1d / Phase L panel-binding
|
||||
/// edits don't accidentally break existing user keymap files.
|
||||
/// </summary>
|
||||
public class KeyBindingsJsonTests
|
||||
{
|
||||
private static string TempFile(string suffix = ".json")
|
||||
{
|
||||
var f = Path.Combine(Path.GetTempPath(),
|
||||
$"acdream_kb_test_{System.Guid.NewGuid():N}{suffix}");
|
||||
return f;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadOrDefault_missing_file_returns_RetailDefaults()
|
||||
{
|
||||
var path = TempFile();
|
||||
// No file written.
|
||||
var loaded = KeyBindings.LoadOrDefault(path);
|
||||
var defaults = KeyBindings.RetailDefaults();
|
||||
Assert.Equal(defaults.All.Count, loaded.All.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Roundtrip_preserves_every_binding()
|
||||
{
|
||||
var path = TempFile();
|
||||
try
|
||||
{
|
||||
var original = KeyBindings.RetailDefaults();
|
||||
original.SaveToFile(path);
|
||||
|
||||
var loaded = KeyBindings.LoadOrDefault(path);
|
||||
|
||||
Assert.Equal(original.All.Count, loaded.All.Count);
|
||||
// Every binding from the original must round-trip with structural
|
||||
// equality (chord + action + activation).
|
||||
foreach (var b in original.All)
|
||||
{
|
||||
Assert.Contains(loaded.All, x =>
|
||||
x.Chord == b.Chord
|
||||
&& x.Action == b.Action
|
||||
&& x.Activation == b.Activation);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadOrDefault_corrupt_file_returns_RetailDefaults()
|
||||
{
|
||||
var path = TempFile();
|
||||
try
|
||||
{
|
||||
File.WriteAllText(path, "{ this is not valid JSON :: garbage");
|
||||
|
||||
var loaded = KeyBindings.LoadOrDefault(path);
|
||||
var defaults = KeyBindings.RetailDefaults();
|
||||
Assert.Equal(defaults.All.Count, loaded.All.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadOrDefault_corrupt_file_does_NOT_overwrite_user_file()
|
||||
{
|
||||
var path = TempFile();
|
||||
try
|
||||
{
|
||||
const string corrupt = "{ this is not valid JSON :: garbage";
|
||||
File.WriteAllText(path, corrupt);
|
||||
|
||||
_ = KeyBindings.LoadOrDefault(path);
|
||||
|
||||
// The corrupt file must remain untouched on disk so the user
|
||||
// can recover or hand-edit. We never silently blow it away.
|
||||
Assert.Equal(corrupt, File.ReadAllText(path));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Custom_binding_for_one_action_keeps_defaults_for_others()
|
||||
{
|
||||
var path = TempFile();
|
||||
try
|
||||
{
|
||||
// User customizes ONE action — replace MovementForward with Q.
|
||||
var custom = new KeyBindings();
|
||||
custom.Add(new(new KeyChord(Key.Q, ModifierMask.None), InputAction.MovementForward));
|
||||
custom.SaveToFile(path);
|
||||
|
||||
var loaded = KeyBindings.LoadOrDefault(path);
|
||||
|
||||
// Custom: MovementForward → Q (only — the user's file
|
||||
// overrode all defaults for that action).
|
||||
var fwd = loaded.ForAction(InputAction.MovementForward).ToList();
|
||||
Assert.Single(fwd);
|
||||
Assert.Equal(new KeyChord(Key.Q, ModifierMask.None), fwd[0].Chord);
|
||||
|
||||
// Default-merged: MovementBackup still has its retail bindings
|
||||
// because the user file didn't customize it.
|
||||
var back = loaded.ForAction(InputAction.MovementBackup).ToList();
|
||||
Assert.Contains(back, x => x.Chord == new KeyChord(Key.X, ModifierMask.None));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadOrDefault_handles_version_zero_legacy_file()
|
||||
{
|
||||
// A pretend "old schema" file with version=0 + just one action.
|
||||
// Should still parse — unknown fields are ignored, missing
|
||||
// actions get default-merged.
|
||||
var path = TempFile();
|
||||
try
|
||||
{
|
||||
const string legacyJson = """
|
||||
{
|
||||
"version": 0,
|
||||
"actions": {
|
||||
"MovementForward": [
|
||||
{ "key": "W", "mod": "None" }
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(path, legacyJson);
|
||||
|
||||
var loaded = KeyBindings.LoadOrDefault(path);
|
||||
|
||||
// The custom binding survives.
|
||||
var fwd = loaded.ForAction(InputAction.MovementForward).ToList();
|
||||
Assert.Single(fwd);
|
||||
|
||||
// Default-merged: the user file didn't define MovementBackup
|
||||
// so it gets the retail default chords.
|
||||
var back = loaded.ForAction(InputAction.MovementBackup).ToList();
|
||||
Assert.NotEmpty(back);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadOrDefault_preserves_Hold_activation()
|
||||
{
|
||||
var path = TempFile();
|
||||
try
|
||||
{
|
||||
var original = new KeyBindings();
|
||||
original.Add(new(
|
||||
new KeyChord(Key.ShiftLeft, ModifierMask.None),
|
||||
InputAction.MovementWalkMode,
|
||||
ActivationType.Hold));
|
||||
original.SaveToFile(path);
|
||||
|
||||
var loaded = KeyBindings.LoadOrDefault(path);
|
||||
var binds = loaded.ForAction(InputAction.MovementWalkMode).ToList();
|
||||
// The user binding is preserved with Hold activation.
|
||||
Assert.Contains(binds, x =>
|
||||
x.Chord.Key == Key.ShiftLeft
|
||||
&& x.Activation == ActivationType.Hold);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadOrDefault_unknown_action_name_skipped_silently()
|
||||
{
|
||||
var path = TempFile();
|
||||
try
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"version": 1,
|
||||
"actions": {
|
||||
"NotARealActionName": [
|
||||
{ "key": "W", "mod": "None" }
|
||||
],
|
||||
"MovementForward": [
|
||||
{ "key": "Q", "mod": "None" }
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(path, json);
|
||||
|
||||
var loaded = KeyBindings.LoadOrDefault(path);
|
||||
// The known action loaded; unknown action skipped without
|
||||
// throwing.
|
||||
var fwd = loaded.ForAction(InputAction.MovementForward).ToList();
|
||||
Assert.Contains(fwd, x => x.Chord.Key == Key.Q);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultPath_lives_under_LocalAppData_acdream()
|
||||
{
|
||||
var path = KeyBindings.DefaultPath();
|
||||
Assert.Contains("acdream", path);
|
||||
Assert.EndsWith("keybinds.json", path);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
using System.Linq;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
using Silk.NET.Input;
|
||||
|
||||
namespace AcDream.UI.Abstractions.Tests.Input;
|
||||
|
||||
/// <summary>
|
||||
/// K.1c: <see cref="KeyBindings.RetailDefaults"/> now returns the full
|
||||
/// retail-faithful preset, byte-precise to
|
||||
/// <c>docs/research/named-retail/retail-default.keymap.txt</c>. These
|
||||
/// tests pin the canonical mappings the user-visible keyboard layout
|
||||
/// shifts to (W/X movement instead of W/S, Z/C strafe instead of Z/X,
|
||||
/// Tab → ToggleChatEntry instead of fly-mode toggle, etc).
|
||||
/// </summary>
|
||||
public class KeyBindingsRetailTests
|
||||
{
|
||||
[Fact]
|
||||
public void MovementForward_bound_to_W_and_Up()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var binds = b.ForAction(InputAction.MovementForward).ToList();
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.W, ModifierMask.None));
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.Up, ModifierMask.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovementBackup_bound_to_X_and_Down_NOT_S()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var binds = b.ForAction(InputAction.MovementBackup).ToList();
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.X, ModifierMask.None));
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.Down, ModifierMask.None));
|
||||
// Retail: S is NOT MovementBackup. S → MovementStop.
|
||||
Assert.DoesNotContain(binds, x => x.Chord.Key == Key.S);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void S_key_is_MovementStop_in_retail()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var stop = b.Find(new KeyChord(Key.S, ModifierMask.None), ActivationType.Press);
|
||||
Assert.NotNull(stop);
|
||||
Assert.Equal(InputAction.MovementStop, stop!.Value.Action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovementStrafeLeft_bound_to_Z_and_AltA_and_AltLeft()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var binds = b.ForAction(InputAction.MovementStrafeLeft).ToList();
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.Z, ModifierMask.None));
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.A, ModifierMask.Alt));
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.Left, ModifierMask.Alt));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovementStrafeRight_bound_to_C_and_AltD_and_AltRight()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var binds = b.ForAction(InputAction.MovementStrafeRight).ToList();
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.C, ModifierMask.None));
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.D, ModifierMask.Alt));
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.Right, ModifierMask.Alt));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sleeping_bound_to_B()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
Assert.Contains(b.ForAction(InputAction.Sleeping),
|
||||
x => x.Chord == new KeyChord(Key.B, ModifierMask.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovementWalkMode_uses_Hold_activation()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var binds = b.ForAction(InputAction.MovementWalkMode).ToList();
|
||||
Assert.NotEmpty(binds);
|
||||
Assert.All(binds, x => Assert.Equal(ActivationType.Hold, x.Activation));
|
||||
Assert.Contains(binds, x => x.Chord.Key == Key.ShiftLeft);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToggleChatEntry_bound_to_Tab_NOT_fly_toggle()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var hit = b.Find(new KeyChord(Key.Tab, ModifierMask.None), ActivationType.Press);
|
||||
Assert.NotNull(hit);
|
||||
Assert.Equal(InputAction.ToggleChatEntry, hit!.Value.Action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LOGOUT_is_Shift_Escape()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var hit = b.Find(new KeyChord(Key.Escape, ModifierMask.Shift), ActivationType.Press);
|
||||
Assert.NotNull(hit);
|
||||
Assert.Equal(InputAction.LOGOUT, hit!.Value.Action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EscapeKey_is_bare_Escape()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var hit = b.Find(new KeyChord(Key.Escape, ModifierMask.None), ActivationType.Press);
|
||||
Assert.NotNull(hit);
|
||||
Assert.Equal(InputAction.EscapeKey, hit!.Value.Action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseQuickSlot_5_bound_to_Number5_with_and_without_Ctrl()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var binds = b.ForAction(InputAction.UseQuickSlot_5).ToList();
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.Number5, ModifierMask.None));
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.Number5, ModifierMask.Ctrl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseQuickSlot_18_bound_to_Alt_Number9()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var binds = b.ForAction(InputAction.UseQuickSlot_18).ToList();
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.Number9, ModifierMask.Alt));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TogglePluginManager_is_Shift_Ctrl_F1()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var binds = b.ForAction(InputAction.TogglePluginManager).ToList();
|
||||
Assert.Contains(binds, x =>
|
||||
x.Chord.Key == Key.F1
|
||||
&& x.Chord.Modifiers.HasFlag(ModifierMask.Shift)
|
||||
&& x.Chord.Modifiers.HasFlag(ModifierMask.Ctrl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Acdream_debug_actions_relocated_to_Ctrl_F_keys()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
Assert.Contains(b.ForAction(InputAction.AcdreamToggleDebugPanel),
|
||||
x => x.Chord == new KeyChord(Key.F1, ModifierMask.Ctrl));
|
||||
Assert.Contains(b.ForAction(InputAction.AcdreamToggleCollisionWires),
|
||||
x => x.Chord == new KeyChord(Key.F2, ModifierMask.Ctrl));
|
||||
Assert.Contains(b.ForAction(InputAction.AcdreamDumpNearby),
|
||||
x => x.Chord == new KeyChord(Key.F3, ModifierMask.Ctrl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToggleHelp_is_bare_F1_NOT_acdream_debug()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var hit = b.Find(new KeyChord(Key.F1, ModifierMask.None), ActivationType.Press);
|
||||
Assert.NotNull(hit);
|
||||
Assert.Equal(InputAction.ToggleHelp, hit!.Value.Action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Total_binding_count_is_at_least_80()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
Assert.True(b.All.Count >= 80,
|
||||
$"Expected at least 80 bindings, got {b.All.Count}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cry_emote_bound_to_U()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
Assert.Contains(b.ForAction(InputAction.Cry),
|
||||
x => x.Chord == new KeyChord(Key.U, ModifierMask.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CombatToggleCombat_bound_to_GraveAccent()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
Assert.Contains(b.ForAction(InputAction.CombatToggleCombat),
|
||||
x => x.Chord == new KeyChord(Key.GraveAccent, ModifierMask.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CameraActivateAlternateMode_bound_to_F2_and_NumpadDivide()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
var binds = b.ForAction(InputAction.CameraActivateAlternateMode).ToList();
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.F2, ModifierMask.None));
|
||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.KeypadDivide, ModifierMask.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScrollUp_bound_to_Ctrl_Up()
|
||||
{
|
||||
var b = KeyBindings.RetailDefaults();
|
||||
Assert.Contains(b.ForAction(InputAction.ScrollUp),
|
||||
x => x.Chord == new KeyChord(Key.Up, ModifierMask.Ctrl));
|
||||
}
|
||||
}
|
||||
|
|
@ -97,13 +97,18 @@ public class KeyBindingsTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void RetailDefaults_proxies_AcdreamCurrentDefaults_in_K1a()
|
||||
public void RetailDefaults_diverges_from_AcdreamCurrentDefaults_in_K1c()
|
||||
{
|
||||
// K.1a stub — RetailDefaults() returns the acdream-current binds so
|
||||
// K.1b cutover doesn't change behavior. K.1c flips this to the
|
||||
// retail preset.
|
||||
// K.1c flips RetailDefaults() to the retail-faithful preset.
|
||||
// The acdream-current map remains accessible (for tests pinning
|
||||
// the older WASD-only behavior), but RetailDefaults is now the
|
||||
// canonical startup source. The two MUST differ — at minimum
|
||||
// RetailDefaults binds X to Backup (retail) where AcdreamCurrent
|
||||
// bound X to StrafeRight, and RetailDefaults binds Tab to
|
||||
// ToggleChatEntry where AcdreamCurrent bound Tab to
|
||||
// AcdreamTogglePlayerMode.
|
||||
var retail = KeyBindings.RetailDefaults();
|
||||
var current = KeyBindings.AcdreamCurrentDefaults();
|
||||
Assert.Equal(current.All.Count, retail.All.Count);
|
||||
Assert.NotEqual(current.All.Count, retail.All.Count);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue