fix(input): focus loss no longer faults on an unpublished movement controller (#356)

Losing window focus calls CameraPointerInputController.HandleFocusChanged
-> MouseLookController.EndForLifecycle -> PlayerMovementController
.EndMouseLook, and EnsurePublishedForRuntimeOperation throws when the
controller exists but is not yet published (mid-login) or already retired
(post-logout). A focus callback can land in either window, so a simple
alt-tab during the login stream took the whole process down with an
unhandled InvalidOperationException. Hit live during the Campaign A
listening-session launches.

EndAndRestoreCursor already guarded 'no controller'; publication state is
the finer-grained form of the same condition, so the guard is completed
with the new CanExecuteLiveMovement predicate (the exact lifecycle set
EnsurePublishedForRuntimeOperation accepts) rather than wrapping the call
in a catch. Cursor restore still runs unconditionally - presentation is
always safe. Published-controller behaviour is unchanged.

Also files issue #357: the login placement stall this session exposed
(reveal ready=True, player Place edge never executes, world never opens).
That one is NOT fixed here - full evidence chain, wire capture, and probe
output are in the issue. It is a placement-domain bug and blocks the
Campaign A listening gate.

Suite: green except the known load-dependent measurement flake
(RuntimeCollisionReportingStateTests allocation pin), which passes in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-09 12:02:38 +02:00
parent 2914e43aa9
commit 972c7ab3b8
3 changed files with 78 additions and 1 deletions

View file

@ -196,8 +196,14 @@ internal sealed class MouseLookController : IMouseLookInputFrameController
bool stateWasActive = _state.Active;
_state.Release();
// The null check alone is not enough: a controller can also be present
// but UNPUBLISHED (mid-login) or retired (post-logout), and a focus-loss
// callback can land in either window. EndMouseLook faults on both, so a
// simple alt-tab during login used to take the process down. Cursor
// restore below still runs — that is presentation and always safe.
PlayerMovementController? controller = _playerController.Controller;
if (controller is not null && controller.EndMouseLook(_movementInput.Capture()))
if (controller is { CanExecuteLiveMovement: true }
&& controller.EndMouseLook(_movementInput.Capture()))
{
_outbound.TrySendMovement(
_session.CurrentSession,