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

@ -0,0 +1,113 @@
using AcDream.App.Input;
using AcDream.UI.Abstractions.Input;
using Silk.NET.Input;
namespace AcDream.App.Tests.Input;
public sealed class RetailKeymapFileTests
{
[Fact]
public void Parse_CommittedRetailFile_ReproducesEveryUserBindableDefault()
{
string text = File.ReadAllText(Path.Combine(
FindRepoRoot(), "docs", "research", "named-retail",
"retail-default.keymap.txt"));
KeyBindings expected = KeyBindings.RetailDefaults();
KeyBindings actual = RetailKeymapFile.Parse(text, expected);
Assert.Equal(
expected.ForAction(InputAction.MovementForward).ToArray(),
actual.ForAction(InputAction.MovementForward).ToArray());
Assert.Equal(
expected.ForAction(InputAction.MovementWalkMode).ToArray(),
actual.ForAction(InputAction.MovementWalkMode).ToArray());
Assert.Equal(
expected.ForAction(InputAction.ToggleInventoryPanel).ToArray(),
actual.ForAction(InputAction.ToggleInventoryPanel).ToArray());
Assert.Equal(
expected.ForAction(InputAction.CameraAlternateRotateLeft).ToArray(),
actual.ForAction(InputAction.CameraAlternateRotateLeft).ToArray());
// The user's captured retail file omits slots 10-13; omission means
// unbound rather than "inherit a compiled default".
Assert.Empty(actual.ForAction(InputAction.UseQuickSlot_10));
// Host-only actions are outside retail's fourteen editable maps and survive.
Assert.Equal(
expected.ForAction(InputAction.AcdreamToggleAudioMute).ToArray(),
actual.ForAction(InputAction.AcdreamToggleAudioMute).ToArray());
}
[Fact]
public void WriteThenParse_RoundTripsAll306RetailActionIdentities()
{
var source = new KeyBindings();
foreach (((uint inputMapId, uint actionId), InputAction action) in
RetailActionIdentityTable.Map)
{
source.Add(new Binding(
new KeyChord(
Key.SuperRight,
ModifierMask.Shift | ModifierMask.Ctrl | ModifierMask.Win),
action,
RetailActionIdentityTable.ActivationFor(inputMapId, actionId),
RetailActionIdentityTable.ScopeForInputMap(inputMapId)));
}
string text = RetailKeymapFile.Write(source);
KeyBindings loaded = RetailKeymapFile.Parse(text, new KeyBindings());
Assert.Equal(306, loaded.All.Count);
foreach (Binding expected in source.All)
Assert.Contains(expected, loaded.All);
Assert.Contains("CharacterOptionCommands", text, StringComparison.Ordinal);
Assert.Contains("AutoRepeatAttacks", text, StringComparison.Ordinal);
Assert.Contains("AFKState", text, StringComparison.Ordinal);
Assert.Contains("DIK_RWIN", text, StringComparison.Ordinal);
Assert.Contains("EscapeKey [ \"\" [ 0 DIK_ESCAPE ] ]", text, StringComparison.Ordinal);
Assert.Contains("TargetedUsage", text, StringComparison.Ordinal);
}
[Fact]
public void ProfileStore_SaveAsSelectsProfile_AndLoadReplacesRetailRows()
{
string root = Path.Combine(Path.GetTempPath(), "acdream-keymap-" + Guid.NewGuid().ToString("N"));
string config = Path.Combine(root, "config");
string documents = Path.Combine(root, "documents", "Asheron's Call");
string json = Path.Combine(config, "keybinds.json");
try
{
var source = KeyBindings.RetailDefaults();
var store = new RetailKeymapProfileStore(json, documents);
RetailKeymapSaveResult saved = store.Save("friends", source, overwrite: false);
Assert.Equal(RetailKeymapSaveStatus.Saved, saved.Status);
Assert.Equal("friends.keymap", store.CurrentFileName);
Assert.Equal(new[] { "friends.keymap" }, store.ListFiles());
Assert.True(store.TryLoad(
"friends.keymap", new KeyBindings(), out KeyBindings loaded, out string? error),
error);
Assert.Equal(
source.ForAction(InputAction.MovementForward).ToArray(),
loaded.ForAction(InputAction.MovementForward).ToArray());
Assert.Equal(
RetailKeymapSaveStatus.Exists,
store.Save("friends", source, overwrite: false).Status);
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
private static string FindRepoRoot()
{
string? directory = AppContext.BaseDirectory;
while (directory is not null)
{
if (File.Exists(Path.Combine(directory, "AcDream.slnx")))
return directory;
directory = Directory.GetParent(directory)?.FullName;
}
throw new DirectoryNotFoundException("Could not locate acdream.sln.");
}
}