fix(network): prevent null multiplayer peer crashes offline/teardown and fix Nakama RPC session validation

This commit is contained in:
2026-07-10 17:36:26 +08:00
parent 2b4c9d9dcb
commit b30709de3d
84 changed files with 5291 additions and 982 deletions
+38 -1
View File
@@ -48,6 +48,12 @@ var server_version := ""
var dispatcher
var log_buffer
var surfaced_error_tracker
## Set by plugin.gd. Lets the per-frame play-state poll end game-run
## bookkeeping when the game exits on its own (self-quit, crash) — the
## debugger session's stopped signal is not reliably connected, and no MCP
## stop op runs in that path (#642).
var debugger_plugin
## Set by plugin.gd when the HTTP port is occupied by an incompatible or
## unverified server. Keeping the Connection node alive lets handlers and the
## dock share one object, but no WebSocket is opened to the wrong server.
@@ -89,6 +95,10 @@ func _process(delta: float) -> void:
if pause_processing:
return
_peer.poll()
## Run-stop bookkeeping must not wait behind the socket-state machine:
## if the game stops while disconnected, the first command drained on
## reconnect would still observe stale "live" state (PR #642 review).
_check_game_run_play_state(EditorInterface.is_playing_scene())
match _peer.get_ready_state():
WebSocketPeer.STATE_OPEN:
@@ -309,6 +319,8 @@ func send_deferred_response(request_id: String, payload: Dictionary) -> void:
## `readiness_after` payload field were ever dropped.
if not response.has("readiness"):
response["readiness"] = get_readiness()
if not response.has("error_watermark"):
_stamp_error_watermark(response)
if _send_json(response) and dispatcher != null:
dispatcher.complete_deferred_response(request_id)
@@ -319,10 +331,15 @@ func _hook_editor_signals() -> void:
EditorInterface.get_editor_settings() # ensure interface is ready
_last_scene_path = _get_current_scene_path()
_last_play_state = EditorInterface.is_playing_scene()
_last_play_state_for_run = _last_play_state
var _last_scene_path := ""
var _last_play_state := false
## Separate edge tracker for game-run bookkeeping: _last_play_state only
## advances when the play_state_changed event sends successfully, but ending
## run tracking must not depend on the websocket being up.
var _last_play_state_for_run := false
var _last_readiness := ""
@@ -359,7 +376,22 @@ func _check_state_changes() -> void:
if send_event("readiness_changed", {"readiness": readiness}):
_last_readiness = readiness
if log_buffer:
log_buffer.log("[event] readiness -> %s" % readiness)
## echo=false: readiness flips on every filesystem scan
## (each import cycles importing -> ready), so echoing to
## console spams every install during normal editing (#626).
## The line stays in the ring for the dock's log panel.
log_buffer.log("[event] readiness -> %s" % readiness, false)
## Playing→stopped edge for game-run bookkeeping. Runs every process tick
## (any socket state) so a self-quit game's run ends even while the
## transport is down or reconnecting.
func _check_game_run_play_state(playing: bool) -> void:
if playing == _last_play_state_for_run:
return
if not playing and debugger_plugin != null:
debugger_plugin.note_editor_play_stopped()
_last_play_state_for_run = playing
func _get_current_scene_path() -> String:
@@ -403,6 +435,7 @@ func _handle_outbound_backpressure(
return false
var err_response := _make_backpressure_error(request_id, buffered_bytes, message_bytes)
_stamp_error_watermark(err_response)
var err_text := JSON.stringify(err_response)
var err_bytes := err_text.to_utf8_buffer().size()
if _would_exceed_outbound_backpressure(buffered_bytes, err_bytes):
@@ -457,6 +490,10 @@ static func _make_backpressure_error(
}
func _stamp_error_watermark(response: Dictionary) -> void:
McpSurfacedErrorTracker.stamp_watermark(response, surfaced_error_tracker)
## Build a human-readable session ID of form "<slug>@<4hex>" from the project path.
## The slug is derived from the project directory name so agents can recognize
## which editor they're targeting; the hex suffix disambiguates same-project twins.