fix(input): free-fly is unreachable, and Escape no longer answers to it

User report: "ESC is hardwired to Freefly which it should not be ... the
freefly should really be discarded. Should not be in the client."

Two separate things were true.

Escape ran a priority chain — cancel target mode, else EXIT FLY MODE, else
leave player mode, else close a window — so in a session that had reached the
free-fly camera, Escape spent itself on that rung instead of doing what the
player expected. The rung is gone; a session somehow in fly mode now falls
through to the next one.

And free-fly was still bound: Ctrl+Shift+F in RetailDefaults (the table
production actually loads) and plain F in AcdreamCurrentDefaults (dead since
K.1c, removed anyway so it cannot be revived by accident). The comment on the
live binding advertised two other ways in — the ImGui View menu and the Debug
panel's "Toggle Free-Fly Mode" button — but BOTH went away with
AcDream.UI.ImGui at Campaign V, so the shortcut was the last route in. It is
now unbound, and a test pins that across both default tables.

This makes free-fly unreachable rather than deleted. The implementation still
spans 25 files (CameraController, FlyCamera, the dispatcher capture, pointer
controller, composition, and a streaming observer source), and ripping that out
at the end of a long session is how a regression lands in the camera. Scoped as
its own follow-up; unbinding is what fixes the reported behaviour today.

The Escape priority test was updated rather than deleted: its middle row now
asserts the fall-through, so the removed rung is documented by a passing test
instead of by its absence.

Solution builds clean; full hermetic gate green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-21 09:55:16 +02:00
parent 550621efb2
commit 111fbba3bb
4 changed files with 45 additions and 13 deletions

View file

@ -239,8 +239,6 @@ internal sealed class GameplayInputCommandController : IGameplayInputCommandTarg
{
if (_targetMode.IsAnyTargetModeActive)
_targetMode.CancelTargetMode();
else if (_camera.IsFlyMode)
_camera.ExitFlyMode();
else if (_playerMode.IsPlayerMode)
_playerMode.ExitPlayerMode();
else

View file

@ -122,7 +122,6 @@ public sealed class KeyBindings
b.Add(new(new KeyChord(Key.F9, ModifierMask.None), InputAction.AcdreamSensitivityUp));
b.Add(new(new KeyChord(Key.M, ModifierMask.Ctrl), InputAction.AcdreamToggleAudioMute));
b.Add(new(new KeyChord(Key.F10, ModifierMask.None), InputAction.AcdreamCycleWeather));
b.Add(new(new KeyChord(Key.F, ModifierMask.None), InputAction.AcdreamToggleFlyMode));
b.Add(new(new KeyChord(Key.Tab, ModifierMask.None), InputAction.AcdreamTogglePlayerMode));
b.Add(new(new KeyChord(Key.Escape, ModifierMask.None), InputAction.EscapeKey));
@ -368,14 +367,15 @@ public sealed class KeyBindings
// collide with anything retail-faithful.
b.Add(new(new KeyChord(Key.M, ModifierMask.Ctrl), InputAction.AcdreamToggleAudioMute));
// K-fix2 (2026-04-26): free-fly toggle keyboard shortcut.
// Retail leaves Ctrl+Shift+F unbound (retail F = SelectionPickUp,
// Ctrl+F = unused) so this is non-conflicting. Also discoverable
// via View → Camera in the ImGui MainMenuBar and the
// "Toggle Free-Fly Mode" button in the Debug panel.
b.Add(new(
new KeyChord(Key.F, ModifierMask.Ctrl | ModifierMask.Shift),
InputAction.AcdreamToggleFlyMode));
// The free-fly camera has NO key binding. It is a developer camera
// retail never had, and a player who reaches it finds the client in a
// state nothing in the retail UI explains. Its two discovery routes
// named by the old comment here — the ImGui View menu and the Debug
// panel button — both went away with AcDream.UI.ImGui at Campaign V,
// so the shortcut was the last way in.
//
// The mode's implementation is still present and is scheduled for
// removal; unbinding it is what makes it unreachable today.
// K-fix1 (2026-04-26): RMB-hold camera orbit. Coexists with the
// SelectRight Click binding above resolves only after a stationary

View file

@ -79,12 +79,22 @@ public sealed class GameplayInputCommandControllerTests
Assert.Empty(harness.Calls);
}
/// <summary>
/// Escape's priority chain: cancel a target mode, else leave player mode,
/// else close a window.
/// </summary>
/// <remarks>
/// The free-fly rung was REMOVED (2026-08-21, user direction: free-fly
/// "should not be in the client"). The middle row below is the proof — a
/// session that is somehow in fly mode now falls through to the next rung
/// rather than silently exiting a camera the player has no way to enter.
/// </remarks>
[Theory]
[InlineData(true, true, true, "cancel-target")]
[InlineData(false, true, true, "exit-fly")]
[InlineData(false, true, true, "exit-player")]
[InlineData(false, false, true, "exit-player")]
[InlineData(false, false, false, "close")]
public void Escape_PreservesTargetFlyPlayerWindowPriority(
public void Escape_PreservesTargetPlayerWindowPriority(
bool targetMode,
bool flyMode,
bool playerMode,

View file

@ -243,4 +243,28 @@ public class KeyBindingsRetailTests
Assert.NotNull(bound);
Assert.Equal(InputAction.ToggleOptionsPanel, bound!.Value.Action);
}
/// <summary>
/// The developer free-fly camera must not be reachable from the keyboard.
/// </summary>
/// <remarks>
/// It is a camera retail never had, and a player who lands in it finds the
/// client in a state nothing in the retail UI explains. Its two other ways
/// in — the ImGui View menu and the Debug panel button — went away with
/// AcDream.UI.ImGui at Campaign V, so a binding was the last route.
/// </remarks>
[Fact]
public void NoDefaultBindingReachesTheFreeFlyCamera()
{
foreach (KeyBindings bindings in new[]
{
KeyBindings.RetailDefaults(),
KeyBindings.AcdreamCurrentDefaults(),
})
{
Assert.DoesNotContain(
bindings.All,
binding => binding.Action == InputAction.AcdreamToggleFlyMode);
}
}
}