From 1d926e705ba5ea8941295d25aebe6dc12a347027 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:44:42 +0100 Subject: [PATCH] fix(multiplayer): preserve planned shutdown reason --- Game/scripts/lobby.gd | 7 +++++++ multiplayer-next.md | 2 ++ 2 files changed, 9 insertions(+) diff --git a/Game/scripts/lobby.gd b/Game/scripts/lobby.gd index dd7a159e..18ef965a 100644 --- a/Game/scripts/lobby.gd +++ b/Game/scripts/lobby.gd @@ -19,6 +19,7 @@ extends Control @onready var _switch_team_button: Button = %SwitchTeamButton @onready var _ready_button: CheckButton = %ReadyButton @onready var _leave_button: Button = %LeaveButton +var _planned_server_shutdown := false func _ready() -> void: @@ -35,6 +36,9 @@ func _ready() -> void: _controls_row.visible = NetworkManager.is_client _refresh() + if not MatchNet.last_server_shutdown_reason.is_empty(): + _planned_server_shutdown = true + _status_label.text = "Server closed: %s" % MatchNet.last_server_shutdown_reason func _process(_delta: float) -> void: @@ -63,10 +67,13 @@ func _on_rejected(reason: String) -> void: func _on_server_shutdown(reason: String) -> void: + _planned_server_shutdown = true _status_label.text = "Server closed: %s" % reason func _on_disconnected_from_server() -> void: + if _planned_server_shutdown: + return get_tree().change_scene_to_file(ScenePaths.MAIN_MENU) diff --git a/multiplayer-next.md b/multiplayer-next.md index b9d53016..f074d54d 100644 --- a/multiplayer-next.md +++ b/multiplayer-next.md @@ -1428,3 +1428,5 @@ An adversarial transaction review found that cancellation released only no-show The documented `server_shutdown` reliable control message is now implemented in `MatchNet`, with bounded reason sanitisation and an authority-only receiver signal. Controlled drain broadcasts `server_draining`; allocated initial-connect cancellation broadcasts its policy reason and waits a transport-flush beat before closing. The 156-test Godot harness covers emission and bounds; full multi-process drain delivery remains a live integration gate. Clients now consume planned shutdowns: the reason is retained for presentation, an in-match client returns to the lobby after the notice, and the generic disconnect callback is fenced so it cannot overwrite that planned transition. Lobby clients surface the reason directly. The complete Godot harness remains green; real two-process drain delivery is still an external runtime gate. + +An adversarial UI review found the lobby’s generic disconnect handler still replaced that message with the main menu immediately afterward. Planned disconnects are now fenced in the lobby, and a lobby reached from an active match restores the retained reason on startup; unplanned disconnects keep the existing main-menu behavior.