refactor(input): own pointer and callback lifetime
Move camera pointer, framebuffer resize, and retained/devtools input edges behind focused reversible owners. Preserve input priority while making shutdown deactivate callbacks before live-session retirement and retry physical detach without stranding transport teardown.
This commit is contained in:
parent
d09e246d3a
commit
8b8afeefa3
42 changed files with 4029 additions and 461 deletions
|
|
@ -27,6 +27,7 @@ public sealed class GameWindow : IDisposable
|
|||
private SilkWindowCallbackBinding? _windowCallbacks;
|
||||
private GL? _gl;
|
||||
private IInputContext? _input;
|
||||
private AcDream.App.Input.QuiescentInputContext? _devToolsInputContext;
|
||||
private TerrainModernRenderer? _terrain;
|
||||
/// <summary>Phase N.5b: terrain_modern.vert/.frag program. Owned by
|
||||
/// <see cref="_terrain"/> at draw time but allocated + disposed here.</summary>
|
||||
|
|
@ -34,16 +35,7 @@ public sealed class GameWindow : IDisposable
|
|||
private CameraController? _cameraController;
|
||||
private IDatReaderWriter? _dats;
|
||||
private readonly AcDream.App.Input.PointerPositionState _pointerPosition = new();
|
||||
private float _lastMouseX
|
||||
{
|
||||
get => _pointerPosition.X;
|
||||
set => _pointerPosition.X = value;
|
||||
}
|
||||
private float _lastMouseY
|
||||
{
|
||||
get => _pointerPosition.Y;
|
||||
set => _pointerPosition.Y = value;
|
||||
}
|
||||
private AcDream.App.Input.CameraPointerInputController? _cameraPointerInput;
|
||||
private Shader? _meshShader;
|
||||
private TextureCache? _textureCache;
|
||||
/// <summary>Phase N.4+: WB-backed rendering pipeline adapter. Always non-null
|
||||
|
|
@ -434,6 +426,8 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
private readonly AcDream.App.Input.LocalPlayerSkillState _localPlayerSkills = new();
|
||||
private readonly AcDream.App.Input.ViewportAspectState _viewportAspect = new();
|
||||
private readonly FramebufferResizeController _framebufferResize;
|
||||
private IFramebufferDevToolsTarget? _framebufferDevToolsTarget;
|
||||
private AcDream.App.Input.PlayerModeController? _playerModeController;
|
||||
private readonly AcDream.App.Interaction.PlayerApproachCompletionState
|
||||
_playerApproachCompletions = new();
|
||||
|
|
@ -444,26 +438,6 @@ public sealed class GameWindow : IDisposable
|
|||
// Phase D.2b-C — live character-sheet assembly + raise flow (extracted
|
||||
// feature class; GameWindow only wires it). Null unless ACDREAM_RETAIL_UI=1.
|
||||
private AcDream.App.UI.Layout.CharacterSheetProvider? _characterSheetProvider;
|
||||
// Mouse sensitivity multipliers — one per camera mode because the visual
|
||||
// feel is very different. Adjust via F8 / F9 for whichever mode is
|
||||
// currently active. Chase default is low because the character + camera
|
||||
// rotating together is overwhelming at fly speeds.
|
||||
private float _sensChase
|
||||
{
|
||||
get => _chaseCameraInput.Sensitivity;
|
||||
set => _chaseCameraInput.Sensitivity = value;
|
||||
}
|
||||
private float _sensFly = 1.0f;
|
||||
private float _sensOrbit = 1.0f;
|
||||
|
||||
// Right-mouse-button held → free-orbit the chase camera around the
|
||||
// player without turning the character. Release leaves the camera at
|
||||
// the orbited position (no snap back).
|
||||
private bool _rmbHeld
|
||||
{
|
||||
get => _chaseCameraInput.RmbOrbitHeld;
|
||||
}
|
||||
|
||||
// Phase K.2 — auto-enter player mode after a successful login. Armed
|
||||
// by LiveSessionHost's entered-world transition; ticked from
|
||||
// OnUpdate; disarmed if the user manually enters fly mode (or any
|
||||
|
|
@ -579,6 +553,7 @@ public sealed class GameWindow : IDisposable
|
|||
_retainedInputCapture);
|
||||
_movementInput = new AcDream.App.Input.DispatcherMovementInputSource(
|
||||
_inputCapture);
|
||||
_framebufferResize = new FramebufferResizeController(_viewportAspect);
|
||||
_datDir = options.DatDir;
|
||||
_worldGameState = worldGameState;
|
||||
_worldEvents = worldEvents;
|
||||
|
|
@ -676,6 +651,8 @@ public sealed class GameWindow : IDisposable
|
|||
_physicsEngine.DataCache = _physicsDataCache;
|
||||
|
||||
_gl = GL.GetApi(_window!);
|
||||
_framebufferResize.BindViewport(
|
||||
new SilkFramebufferViewportTarget(_gl));
|
||||
_gpuFrameFlights = new AcDream.App.Rendering.GpuFrameFlightController(_gl);
|
||||
var worldRenderDiagnostics = new AcDream.App.Rendering.WorldRenderDiagnostics(
|
||||
new AcDream.App.Rendering.SilkRenderGlStateReader(_gl),
|
||||
|
|
@ -692,113 +669,36 @@ public sealed class GameWindow : IDisposable
|
|||
// per K.1b plan §D).
|
||||
var firstKb = _input.Keyboards.FirstOrDefault();
|
||||
var firstMouse = _input.Mice.FirstOrDefault();
|
||||
if (firstKb is not null && firstMouse is not null)
|
||||
if (firstKb is not null)
|
||||
{
|
||||
_kbSource = new AcDream.App.Input.SilkKeyboardSource(firstKb);
|
||||
_mouseSource = new AcDream.App.Input.SilkMouseSource(
|
||||
_kbSource = AcDream.App.Input.SilkKeyboardSource.CreateDetached(
|
||||
firstKb,
|
||||
_hostQuiescence);
|
||||
_kbSource.Attach();
|
||||
}
|
||||
if (firstMouse is not null)
|
||||
{
|
||||
_mouseSource = AcDream.App.Input.SilkMouseSource.CreateDetached(
|
||||
firstMouse,
|
||||
_inputCapture,
|
||||
_kbSource);
|
||||
_inputDispatcher = new AcDream.UI.Abstractions.Input.InputDispatcher(
|
||||
_kbSource,
|
||||
_hostQuiescence);
|
||||
_mouseSource.Attach();
|
||||
_mouseLookCursor = new AcDream.App.Input.SilkMouseLookCursor(firstMouse);
|
||||
}
|
||||
if (_kbSource is not null && _mouseSource is not null)
|
||||
{
|
||||
_inputDispatcher = AcDream.UI.Abstractions.Input.InputDispatcher.CreateDetached(
|
||||
_kbSource, _mouseSource, _keyBindings);
|
||||
_inputDispatcher.Attach();
|
||||
_movementInput.Bind(_inputDispatcher);
|
||||
_cameraInput.Bind(_inputDispatcher);
|
||||
_mouseLookCursor = new AcDream.App.Input.SilkMouseLookCursor(firstMouse);
|
||||
_inputDispatcher.Fired += OnInputAction;
|
||||
Combat.CombatModeChanged += SetInputCombatScope;
|
||||
SetInputCombatScope(Combat.CurrentMode);
|
||||
|
||||
}
|
||||
|
||||
// Mouse delta handler — kept direct because Silk.NET delivers mouse
|
||||
// moves as continuous (x, y) positions, not chord events. We gate
|
||||
// on WantCaptureMouse so panel hover never drives camera. The
|
||||
// previous "_playerMouseDeltaX += dx * sens" branch is GONE — mouse
|
||||
// never drives character yaw in K.1b. RMB-held orbit is wired via
|
||||
// the dispatcher's AcdreamRmbOrbitHold action (subscriber sets
|
||||
// _rmbHeld below).
|
||||
foreach (var mouse in _input.Mice)
|
||||
{
|
||||
mouse.MouseMove += (m, pos) =>
|
||||
{
|
||||
// K.1b §E: explicit WantCaptureMouse defense-in-depth on the
|
||||
// surviving direct-mouse handler. Suppresses RMB orbit /
|
||||
// FlyCamera look while ImGui has the mouse focus.
|
||||
if (_inputCapture.WantCaptureMouse)
|
||||
{
|
||||
_lastMouseX = pos.X;
|
||||
_lastMouseY = pos.Y;
|
||||
return;
|
||||
}
|
||||
if (_cameraController is null) return;
|
||||
|
||||
float dx = pos.X - _lastMouseX;
|
||||
float dy = pos.Y - _lastMouseY;
|
||||
|
||||
if (_playerMode && _cameraController.IsChaseMode && _chaseCamera is not null)
|
||||
{
|
||||
float sens = _sensChase;
|
||||
if (_gameplayInputFrame?.MouseLookActive == true)
|
||||
{
|
||||
// DirectInput may deliver several device records before
|
||||
// one retail GetInput pass. Accumulate raw motion here;
|
||||
// the update loop filters the aggregate exactly once.
|
||||
_gameplayInputFrame.QueueRawMouseDelta(dx, dy);
|
||||
}
|
||||
else if (_rmbHeld)
|
||||
{
|
||||
// Hold-RMB orbit: player stays the central point, camera
|
||||
// free-orbits around. X rotates around, Y pitches. On release
|
||||
// the camera STAYS at the new angle (no snap back).
|
||||
// K.1b: this is the ONLY mouse-delta path that affects
|
||||
// ANYTHING when in player mode — character yaw is
|
||||
// dispatcher-only (A/D keys). K.2: MMB mouse-look path
|
||||
// above takes precedence when active.
|
||||
if (AcDream.Core.Rendering.CameraDiagnostics.UseRetailChaseCamera && _retailChaseCamera is not null)
|
||||
{
|
||||
float nowSec = (float)(Environment.TickCount64 / 1000.0);
|
||||
var (filteredDx, filteredDy) = _retailChaseCamera.FilterMouseDelta(
|
||||
rawX: dx, rawY: dy, weight: 0.5f, nowSec: nowSec);
|
||||
_retailChaseCamera.YawOffset -= filteredDx * 0.004f * sens;
|
||||
_retailChaseCamera.AdjustPitch(filteredDy * 0.003f * sens);
|
||||
}
|
||||
else
|
||||
{
|
||||
_chaseCamera.YawOffset -= dx * 0.004f * sens;
|
||||
_chaseCamera.AdjustPitch(dy * 0.003f * sens);
|
||||
}
|
||||
}
|
||||
// K-fix1 (2026-04-26): no default-pitch path. With
|
||||
// neither MMB nor RMB held, mouse moves the cursor
|
||||
// freely so the user can interact with panels +
|
||||
// (eventually) click selectables in the world. Pitch
|
||||
// is gated on a deliberate hold input.
|
||||
}
|
||||
else if (_cameraController.IsFlyMode)
|
||||
{
|
||||
float sens = _sensFly;
|
||||
_cameraController.Fly.Look(dx * sens, dy * sens);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Orbit-camera mode (offline / pre-login): hold LMB to
|
||||
// drag-rotate. K.1b reads the mouse-button state via
|
||||
// IMouseSource so all "is button held" queries go
|
||||
// through the same abstraction the dispatcher uses.
|
||||
float sens = _sensOrbit;
|
||||
if (_mouseSource is not null && _mouseSource.IsHeld(MouseButton.Left))
|
||||
{
|
||||
_cameraController.Orbit.Yaw -= dx * 0.005f * sens;
|
||||
_cameraController.Orbit.Pitch = Math.Clamp(
|
||||
_cameraController.Orbit.Pitch + dy * 0.005f * sens,
|
||||
0.1f, 1.5f);
|
||||
}
|
||||
}
|
||||
_lastMouseX = pos.X;
|
||||
_lastMouseY = pos.Y;
|
||||
};
|
||||
}
|
||||
|
||||
_gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
|
||||
|
|
@ -849,10 +749,26 @@ public sealed class GameWindow : IDisposable
|
|||
Console.WriteLine("world-hud font: no system monospace font found");
|
||||
}
|
||||
|
||||
var orbit = new OrbitCamera { Aspect = _window!.Size.X / (float)_window.Size.Y };
|
||||
var fly = new FlyCamera { Aspect = _window.Size.X / (float)_window.Size.Y };
|
||||
var orbit = new OrbitCamera();
|
||||
var fly = new FlyCamera();
|
||||
_cameraController = new CameraController(orbit, fly);
|
||||
_cameraController.ModeChanged += OnCameraModeChanged;
|
||||
_framebufferResize.BindCamera(
|
||||
new CameraFramebufferTarget(_cameraController));
|
||||
_framebufferResize.Resize(_window!.FramebufferSize);
|
||||
if (_mouseSource is not null && firstMouse is not null)
|
||||
{
|
||||
_cameraPointerInput = AcDream.App.Input.CameraPointerInputController.Create(
|
||||
_input.Mice,
|
||||
_hostQuiescence,
|
||||
_inputCapture,
|
||||
_localPlayerMode,
|
||||
_cameraController,
|
||||
_chaseCameraInput,
|
||||
_mouseSource,
|
||||
_pointerPosition,
|
||||
new AcDream.App.Input.EnvironmentInputMonotonicClock());
|
||||
_cameraPointerInput.AttachRaw();
|
||||
}
|
||||
|
||||
_dats = RuntimeDatCollectionFactory.OpenReadOnly(_datDir);
|
||||
_magicCatalog = AcDream.App.Spells.MagicCatalog.Load(_dats);
|
||||
|
|
@ -959,8 +875,15 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.UI.ImGui.ImGuiBootstrapper? imguiBootstrap = null;
|
||||
try
|
||||
{
|
||||
_devToolsInputContext = new AcDream.App.Input.QuiescentInputContext(
|
||||
_input!,
|
||||
_hostQuiescence);
|
||||
imguiBootstrap =
|
||||
new AcDream.UI.ImGui.ImGuiBootstrapper(_gl!, _window!, _input!);
|
||||
new AcDream.UI.ImGui.ImGuiBootstrapper(
|
||||
_gl!,
|
||||
_window!,
|
||||
_devToolsInputContext);
|
||||
_devToolsInputContext.Activate();
|
||||
var panelHost = new AcDream.UI.ImGui.ImGuiPanelHost();
|
||||
|
||||
// VitalsVM: GUID=0 at construction; set later at EnterWorld
|
||||
|
|
@ -1021,9 +944,11 @@ public sealed class GameWindow : IDisposable
|
|||
_worldSceneDebugState.CollisionWireframesVisible,
|
||||
getStreamingRadius: () => _nearRadius, // A.5 T16 follow-up: was _streamingRadius (legacy single-tier); show near tier
|
||||
|
||||
getMouseSensitivity: () => GetActiveSensitivity(),
|
||||
getMouseSensitivity: () =>
|
||||
_cameraPointerInput?.ActiveSensitivity ?? 1f,
|
||||
getChaseDistance: () => _chaseCamera?.Distance ?? 0f,
|
||||
getRmbOrbit: () => _rmbHeld,
|
||||
getRmbOrbit: () =>
|
||||
_cameraPointerInput?.RmbOrbitHeld ?? false,
|
||||
getHourName: () => WorldTime.CurrentCalendar.Hour.ToString(),
|
||||
getDayFraction: () => (float)WorldTime.DayFraction,
|
||||
getWeather: () => Weather.Kind.ToString(),
|
||||
|
|
@ -1229,6 +1154,9 @@ public sealed class GameWindow : IDisposable
|
|||
debugPanel,
|
||||
_debugVm,
|
||||
settingsPanel));
|
||||
_framebufferDevToolsTarget = new DevToolsFramebufferTarget(
|
||||
_devToolsFramePresenter);
|
||||
_framebufferResize.BindDevTools(_framebufferDevToolsTarget);
|
||||
_devToolsFramePresenter.ResetLayout(
|
||||
_window!.Size.X,
|
||||
_window.Size.Y,
|
||||
|
|
@ -1241,6 +1169,13 @@ public sealed class GameWindow : IDisposable
|
|||
// operation. Construction failure still owns this local and
|
||||
// must release it before abandoning the optional frontend.
|
||||
imguiBootstrap?.Dispose();
|
||||
if (_framebufferDevToolsTarget is { } framebufferTarget)
|
||||
{
|
||||
_framebufferResize.UnbindDevTools(framebufferTarget);
|
||||
_framebufferDevToolsTarget = null;
|
||||
}
|
||||
_devToolsInputContext?.Dispose();
|
||||
_devToolsInputContext = null;
|
||||
_devToolsBackend = null;
|
||||
_devToolsFramePresenter = null;
|
||||
_devToolsCameraMenu = null;
|
||||
|
|
@ -1429,7 +1364,11 @@ public sealed class GameWindow : IDisposable
|
|||
if (_options.RetailUi)
|
||||
{
|
||||
_vitalsVm ??= new AcDream.UI.Abstractions.Panels.Vitals.VitalsVM(Combat, LocalPlayer);
|
||||
_uiHost = new AcDream.App.UI.UiHost(_gl, shadersDir, _debugFont);
|
||||
_uiHost = new AcDream.App.UI.UiHost(
|
||||
_gl,
|
||||
shadersDir,
|
||||
_debugFont,
|
||||
_hostQuiescence);
|
||||
_retainedInputCapture.Root = _uiHost.Root;
|
||||
_uiHost.Root.UiLocked = _persistedGameplay.LockUI;
|
||||
var cursorFeedbackController = new AcDream.App.UI.CursorFeedbackController(
|
||||
|
|
@ -2031,7 +1970,9 @@ public sealed class GameWindow : IDisposable
|
|||
camera.Projection,
|
||||
camera.Viewport);
|
||||
},
|
||||
() => new System.Numerics.Vector2(_lastMouseX, _lastMouseY),
|
||||
() => new System.Numerics.Vector2(
|
||||
_pointerPosition.X,
|
||||
_pointerPosition.Y),
|
||||
() => _playerController is { } player
|
||||
? new AcDream.App.Interaction.PlayerInteractionPose(
|
||||
player.CellId,
|
||||
|
|
@ -2472,6 +2413,7 @@ public sealed class GameWindow : IDisposable
|
|||
mouseLookController,
|
||||
new AcDream.App.Input.CombatAttackInputFrameAdapter(
|
||||
_combatAttackController!));
|
||||
_cameraPointerInput?.BindGameplayFrame(_gameplayInputFrame);
|
||||
var streamingFrame = new AcDream.App.Streaming.StreamingFrameController(
|
||||
_options.LiveMode,
|
||||
_localPlayerMode,
|
||||
|
|
@ -2559,7 +2501,6 @@ public sealed class GameWindow : IDisposable
|
|||
_animatedEntities,
|
||||
_equippedChildRenderer!,
|
||||
liveEffectFrame);
|
||||
_viewportAspect.Update(_window!.Size.X, _window.Size.Y);
|
||||
_playerModeController = new AcDream.App.Input.PlayerModeController(
|
||||
_localPlayerMode,
|
||||
_playerControllerSlot,
|
||||
|
|
@ -3169,31 +3110,6 @@ public sealed class GameWindow : IDisposable
|
|||
new AcDream.App.Update.UpdateFrameInput(dt));
|
||||
}
|
||||
|
||||
private void OnCameraModeChanged(bool _modeBool)
|
||||
{
|
||||
if (_gameplayInputFrame?.MouseLookActive == true
|
||||
&& _cameraController?.IsChaseMode != true)
|
||||
{
|
||||
_gameplayInputFrame?.EndMouseLook();
|
||||
}
|
||||
|
||||
if (_input is null) return;
|
||||
var mouse = _input.Mice.FirstOrDefault();
|
||||
if (mouse is null) return;
|
||||
|
||||
// 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 the gameplay input owner.
|
||||
bool needsRawCursor = _cameraController?.IsFlyMode == true;
|
||||
mouse.Cursor.CursorMode = needsRawCursor ? CursorMode.Raw : CursorMode.Normal;
|
||||
}
|
||||
|
||||
// Performance overlay state — updated every ~0.5s and written to the
|
||||
// window title so there's zero rendering cost (no font/overlay needed).
|
||||
private void OnRender(double deltaSeconds)
|
||||
|
|
@ -3325,13 +3241,6 @@ public sealed class GameWindow : IDisposable
|
|||
private bool GetDebugPlayerOnGround() =>
|
||||
_playerMode && _playerController is not null && !_playerController.IsAirborne;
|
||||
|
||||
private float GetActiveSensitivity()
|
||||
{
|
||||
if (_playerMode && _cameraController?.IsChaseMode == true) return _sensChase;
|
||||
if (_cameraController?.IsFlyMode == true) return _sensFly;
|
||||
return _sensOrbit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cycle the time-of-day debug override. Same body as the old F7
|
||||
/// keybind handler; called by both the keybind AND the DebugPanel
|
||||
|
|
@ -3611,21 +3520,7 @@ public sealed class GameWindow : IDisposable
|
|||
/// previously off the new viewport snap back to default positions.
|
||||
/// </summary>
|
||||
private void OnFramebufferResize(Silk.NET.Maths.Vector2D<int> newSize)
|
||||
{
|
||||
if (newSize.X <= 0 || newSize.Y <= 0) return;
|
||||
_gl?.Viewport(0, 0, (uint)newSize.X, (uint)newSize.Y);
|
||||
_viewportAspect.Update(newSize.X, newSize.Y);
|
||||
_cameraController?.SetAspect(newSize.X / (float)newSize.Y);
|
||||
// Resize is always a force-reset — the alternative ("clamp
|
||||
// existing positions") would require tracking each panel's
|
||||
// current pos+size, which ImGuiNET doesn't expose by name.
|
||||
// Force-reset is acceptable UX because resizing happens rarely
|
||||
// and the user can always drag panels back where they want.
|
||||
_devToolsFramePresenter?.ResetLayout(
|
||||
newSize.X,
|
||||
newSize.Y,
|
||||
AcDream.App.Rendering.DevToolsPanelLayoutCondition.Always);
|
||||
}
|
||||
=> _framebufferResize.Resize(newSize);
|
||||
|
||||
/// <summary>
|
||||
/// L.0 Display tab: apply the window-state-dependent settings
|
||||
|
|
@ -3701,11 +3596,8 @@ public sealed class GameWindow : IDisposable
|
|||
// Diagnostic — kept from K.1a; helpful for K.1c verification.
|
||||
Console.WriteLine($"[input] {action} {activation}");
|
||||
|
||||
// RMB-orbit hold: track press/release transitions explicitly so
|
||||
// _rmbHeld is true exactly while the chord is held. Hold-type
|
||||
// chords also fire Press on key-down + Release on key-up; we
|
||||
// ignore the in-between Hold ticks here (the mouse-move handler
|
||||
// checks _rmbHeld each frame anyway).
|
||||
// RMB orbit and instant mouse-look remain the first accepted input
|
||||
// transition. Their state is owned by the gameplay/pointer owners.
|
||||
if (_gameplayInputFrame?.HandlePointerAction(action, activation) == true)
|
||||
return;
|
||||
|
||||
|
|
@ -3958,20 +3850,10 @@ public sealed class GameWindow : IDisposable
|
|||
/// </summary>
|
||||
private void AdjustActiveSensitivity(float factor)
|
||||
{
|
||||
string modeLabel;
|
||||
float current;
|
||||
if (_playerMode && _cameraController?.IsChaseMode == true)
|
||||
{ modeLabel = "Chase"; current = _sensChase; }
|
||||
else if (_cameraController?.IsFlyMode == true)
|
||||
{ modeLabel = "Fly"; current = _sensFly; }
|
||||
else
|
||||
{ modeLabel = "Orbit"; current = _sensOrbit; }
|
||||
|
||||
float next = MathF.Min(3.0f, MathF.Max(0.005f, current * factor));
|
||||
if (modeLabel == "Chase") _sensChase = next;
|
||||
else if (modeLabel == "Fly") _sensFly = next;
|
||||
else _sensOrbit = next;
|
||||
_debugVm?.AddToast($"{modeLabel} sens {next:F3}x");
|
||||
if (_cameraPointerInput is not { } pointer)
|
||||
return;
|
||||
string message = pointer.AdjustSensitivity(factor);
|
||||
_debugVm?.AddToast(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -4040,28 +3922,7 @@ public sealed class GameWindow : IDisposable
|
|||
/// retail wheel feel).
|
||||
/// </summary>
|
||||
private void HandleScrollAction(AcDream.UI.Abstractions.Input.InputAction action)
|
||||
{
|
||||
if (_cameraController is null) return;
|
||||
float dir = (action == AcDream.UI.Abstractions.Input.InputAction.ScrollUp) ? 1f : -1f;
|
||||
|
||||
if (_playerMode && _cameraController.IsChaseMode)
|
||||
{
|
||||
// Chase mode: zoom (closer on ScrollUp).
|
||||
if (AcDream.Core.Rendering.CameraDiagnostics.UseRetailChaseCamera && _retailChaseCamera is not null)
|
||||
_retailChaseCamera.AdjustDistance(-dir * 0.8f);
|
||||
else if (_chaseCamera is not null)
|
||||
_chaseCamera.AdjustDistance(-dir * 0.8f);
|
||||
}
|
||||
else if (_cameraController.IsFlyMode)
|
||||
{
|
||||
// Fly mode: no-op (could adjust move speed later).
|
||||
}
|
||||
else
|
||||
{
|
||||
_cameraController.Orbit.Distance = Math.Clamp(
|
||||
_cameraController.Orbit.Distance - dir * 20f, 50f, 2000f);
|
||||
}
|
||||
}
|
||||
=> _cameraPointerInput?.HandleScroll(action);
|
||||
|
||||
private void OnClosing()
|
||||
{
|
||||
|
|
@ -4088,6 +3949,20 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
private ResourceShutdownTransaction CreateShutdownTransaction() => new(
|
||||
// Logical cutoff precedes the live session's potentially long graceful
|
||||
// close. Physical event removal follows session retirement, so a bad
|
||||
// Silk accessor cannot prevent F653/transport teardown. The input
|
||||
// context and UI owners remain alive for reset, while copied callbacks
|
||||
// are already inert.
|
||||
new ResourceShutdownStage("input callback deactivation",
|
||||
[
|
||||
new("camera pointer", () => _cameraPointerInput?.Deactivate()),
|
||||
new("dispatcher", () => _inputDispatcher?.Deactivate()),
|
||||
new("mouse source", () => _mouseSource?.Deactivate()),
|
||||
new("keyboard source", () => _kbSource?.Deactivate()),
|
||||
new("retained UI input", () => _uiHost?.QuiesceInput()),
|
||||
new("developer tools input", () => _devToolsInputContext?.Deactivate()),
|
||||
]),
|
||||
// Live-session reset retires the complete streaming window. Every
|
||||
// reset callback owner, especially LandblockStreamer, must remain live
|
||||
// until that transaction converges.
|
||||
|
|
@ -4114,6 +3989,57 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}),
|
||||
]),
|
||||
new ResourceShutdownStage("input callback detach",
|
||||
[
|
||||
new("retained UI input", () => _uiHost?.DeactivateInput()),
|
||||
new("developer tools input", () => _devToolsInputContext?.Dispose()),
|
||||
new("camera pointer", () =>
|
||||
{
|
||||
AcDream.App.Input.CameraPointerInputController? pointer =
|
||||
_cameraPointerInput;
|
||||
if (pointer is null) return;
|
||||
pointer.Dispose();
|
||||
if (!pointer.IsDisposalComplete)
|
||||
throw new InvalidOperationException(
|
||||
"Camera pointer callback removal remains pending.");
|
||||
}),
|
||||
new("combat input subscription", () =>
|
||||
Combat.CombatModeChanged -= SetInputCombatScope),
|
||||
new("dispatcher", () =>
|
||||
{
|
||||
AcDream.UI.Abstractions.Input.InputDispatcher? dispatcher =
|
||||
_inputDispatcher;
|
||||
if (dispatcher is null) return;
|
||||
dispatcher.Fired -= OnInputAction;
|
||||
_movementInput.Unbind(dispatcher);
|
||||
_cameraInput.Unbind(dispatcher);
|
||||
dispatcher.Dispose();
|
||||
if (!dispatcher.IsDisposalComplete)
|
||||
throw new InvalidOperationException(
|
||||
"Input dispatcher source removal remains pending.");
|
||||
_inputDispatcher = null;
|
||||
}),
|
||||
new("mouse source", () =>
|
||||
{
|
||||
AcDream.App.Input.SilkMouseSource? source = _mouseSource;
|
||||
if (source is null) return;
|
||||
source.Dispose();
|
||||
if (!source.IsDisposalComplete)
|
||||
throw new InvalidOperationException(
|
||||
"Mouse source callback removal remains pending.");
|
||||
_mouseSource = null;
|
||||
}),
|
||||
new("keyboard source", () =>
|
||||
{
|
||||
AcDream.App.Input.SilkKeyboardSource? source = _kbSource;
|
||||
if (source is null) return;
|
||||
source.Dispose();
|
||||
if (!source.IsDisposalComplete)
|
||||
throw new InvalidOperationException(
|
||||
"Keyboard source callback removal remains pending.");
|
||||
_kbSource = null;
|
||||
}),
|
||||
]),
|
||||
// Frame composition borrows equipped, effect, audio, and render
|
||||
// owners. Withdraw the graph before the first borrowed owner closes;
|
||||
// the later render-frontend stage still disposes the GL owners.
|
||||
|
|
@ -4126,7 +4052,16 @@ public sealed class GameWindow : IDisposable
|
|||
]),
|
||||
new ResourceShutdownStage("session dependents",
|
||||
[
|
||||
new("mouse capture", () => _gameplayInputFrame?.EndMouseLook()),
|
||||
new("mouse capture", () =>
|
||||
{
|
||||
AcDream.App.Input.CameraPointerInputController? pointer =
|
||||
_cameraPointerInput;
|
||||
if (pointer is null) return;
|
||||
pointer.ReleaseMouseLookAfterSessionRetirement();
|
||||
if (_gameplayInputFrame is { } gameplayFrame)
|
||||
pointer.UnbindGameplayFrame(gameplayFrame);
|
||||
_cameraPointerInput = null;
|
||||
}),
|
||||
new("retail UI", () =>
|
||||
{
|
||||
_retailUiRuntime?.Dispose();
|
||||
|
|
@ -4196,11 +4131,17 @@ public sealed class GameWindow : IDisposable
|
|||
[
|
||||
new("developer tools", () =>
|
||||
{
|
||||
if (_framebufferDevToolsTarget is { } framebufferTarget)
|
||||
{
|
||||
_framebufferResize.UnbindDevTools(framebufferTarget);
|
||||
_framebufferDevToolsTarget = null;
|
||||
}
|
||||
_devToolsFramePresenter = null;
|
||||
_devToolsCameraMenu = null;
|
||||
_devToolsCommandBus = null;
|
||||
_devToolsBackend?.Dispose();
|
||||
_devToolsBackend = null;
|
||||
_devToolsInputContext = null;
|
||||
}),
|
||||
new("portal tunnel", () =>
|
||||
{
|
||||
|
|
@ -4281,8 +4222,6 @@ public sealed class GameWindow : IDisposable
|
|||
if (ReferenceEquals(_windowCallbacks, binding))
|
||||
_windowCallbacks = null;
|
||||
}),
|
||||
new("combat input subscription", ()
|
||||
=> Combat.CombatModeChanged -= SetInputCombatScope),
|
||||
new("input context", () =>
|
||||
{
|
||||
_input?.Dispose();
|
||||
|
|
@ -4299,10 +4238,7 @@ public sealed class GameWindow : IDisposable
|
|||
]));
|
||||
|
||||
private void OnFocusChanged(bool focused)
|
||||
{
|
||||
if (!focused)
|
||||
_gameplayInputFrame?.EndMouseLook();
|
||||
}
|
||||
=> _cameraPointerInput?.HandleFocusChanged(focused);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue