diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 56f8b74..77e5524 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -3987,19 +3987,23 @@ public sealed class GameWindow : IDisposable return new System.Numerics.Quaternion(0f, 0f, z, w); } - private void OnCameraModeChanged(bool isFlyMode) + private void OnCameraModeChanged(bool _modeBool) { if (_input is null) return; var mouse = _input.Mice.FirstOrDefault(); if (mouse is null) return; - // K-fix1 (2026-04-26): cursor visible by default in chase / orbit - // modes — the user needs to click panels, dropdowns, the future - // selection picker, etc. Mouse-look (raw mode) only happens - // transiently while MMB is held (HideCursorForMouseLook / - // RestoreCursorAfterMouseLook). Fly mode still needs raw because - // it's a continuous look-and-fly affordance. - bool needsRawCursor = isFlyMode; + // K-fix2 (2026-04-26): the bool passed to ModeChanged is NOT + // reliably "isFlyMode" — CameraController.EnterChaseMode invokes + // it with IsChaseMode (true), CameraController.ToggleFly invokes + // it with IsFlyMode, and CameraController.ExitChaseMode invokes + // it with IsFlyMode. Reading the controller state directly is + // the only correct gate. Cursor visible by default in chase / + // orbit modes; Raw cursor only in fly mode (continuous + // look-and-fly affordance). Mouse-look (raw mode) when MMB is + // held is handled separately by HideCursorForMouseLook / + // RestoreCursorAfterMouseLook. + bool needsRawCursor = _cameraController?.IsFlyMode == true; mouse.Cursor.CursorMode = needsRawCursor ? CursorMode.Raw : CursorMode.Normal; _capturedMouse = needsRawCursor ? mouse : null; } @@ -4330,10 +4334,27 @@ public sealed class GameWindow : IDisposable && ImGuiNET.ImGui.MenuItem("Chat")) _chatPanel.IsVisible = !_chatPanel.IsVisible; if (_debugPanel is not null - && ImGuiNET.ImGui.MenuItem("Debug", "F1")) + && ImGuiNET.ImGui.MenuItem("Debug", "Ctrl+F1")) _debugPanel.IsVisible = !_debugPanel.IsVisible; ImGuiNET.ImGui.EndMenu(); } + // K-fix2 (2026-04-26): Camera submenu — discoverable + // free-fly toggle for users who don't know the + // Ctrl+Shift+F shortcut or the Debug-panel button. + if (ImGuiNET.ImGui.BeginMenu("Camera")) + { + if (_cameraController is not null) + { + string flyLabel = _cameraController.IsFlyMode + ? "Exit Free-Fly Mode" : "Enter Free-Fly Mode"; + if (ImGuiNET.ImGui.MenuItem(flyLabel, "Ctrl+Shift+F")) + { + _playerModeAutoEntry?.Cancel(); + _cameraController.ToggleFly(); + } + } + ImGuiNET.ImGui.EndMenu(); + } ImGuiNET.ImGui.EndMainMenuBar(); } @@ -5327,6 +5348,12 @@ public sealed class GameWindow : IDisposable break; case AcDream.UI.Abstractions.Input.InputAction.AcdreamToggleFlyMode: + // K-fix2 (2026-04-26): manual fly toggle pre-empts the + // auto-entry trigger so the chase camera doesn't snap on + // top of the fly camera mid-inspection. Mirrors the + // DebugPanel "Toggle Free-Fly Mode" button + Camera menu + // entry. + _playerModeAutoEntry?.Cancel(); _cameraController?.ToggleFly(); break; diff --git a/src/AcDream.UI.Abstractions/Input/KeyBindings.cs b/src/AcDream.UI.Abstractions/Input/KeyBindings.cs index d2020b0..f5c3c4f 100644 --- a/src/AcDream.UI.Abstractions/Input/KeyBindings.cs +++ b/src/AcDream.UI.Abstractions/Input/KeyBindings.cs @@ -325,12 +325,11 @@ public sealed class KeyBindings b.Add(new(new KeyChord(Key.Down, ModifierMask.Ctrl), InputAction.ScrollDown)); // ── Acdream debug actions: relocated to Ctrl+F* to avoid retail - // conflicts. AcdreamToggleFlyMode + AcdreamTogglePlayerMode have - // NO keyboard binding in retail-default; K.2 adds a DebugPanel - // button for free-fly toggle and player-mode is auto-entered at - // login. AcdreamRmbOrbitHold is intentionally absent — retail - // binds RMB to SelectRight; the chase-camera orbit lives behind - // a debug-mode flag in K.2. + // conflicts. AcdreamTogglePlayerMode has NO keyboard binding + // in retail-default (player-mode is auto-entered at login). + // AcdreamRmbOrbitHold is intentionally absent — retail binds + // RMB to SelectRight; the chase-camera orbit lives behind a + // debug-mode flag in K.2. b.Add(new(new KeyChord(Key.F1, ModifierMask.Ctrl), InputAction.AcdreamToggleDebugPanel)); b.Add(new(new KeyChord(Key.F2, ModifierMask.Ctrl), InputAction.AcdreamToggleCollisionWires)); b.Add(new(new KeyChord(Key.F3, ModifierMask.Ctrl), InputAction.AcdreamDumpNearby)); @@ -339,6 +338,15 @@ public sealed class KeyBindings b.Add(new(new KeyChord(Key.F9, ModifierMask.Ctrl), InputAction.AcdreamSensitivityUp)); b.Add(new(new KeyChord(Key.F10, ModifierMask.Ctrl), InputAction.AcdreamCycleWeather)); + // 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)); + // K-fix1 (2026-04-26): RMB-hold camera orbit. Coexists with the // SelectRight Press binding above — Press fires on click, // AcdreamRmbOrbitHold fires on hold/release transitions so the