fix(network): prevent null multiplayer peer crashes offline/teardown and fix Nakama RPC session validation
This commit is contained in:
@@ -143,9 +143,17 @@ func create_scene(params: Dictionary) -> Dictionary:
|
||||
}
|
||||
|
||||
|
||||
## How long open_scene waits for the editor to actually switch to the
|
||||
## requested scene before replying switched=false. Tab switches normally land
|
||||
## within a few frames; keep this under the dispatcher's 4500 ms deferred
|
||||
## default so the coroutine always answers before DEFERRED_TIMEOUT fires.
|
||||
const _OPEN_SETTLE_MAX_MSEC := 3000
|
||||
|
||||
|
||||
## Open an existing scene by file path.
|
||||
func open_scene(params: Dictionary) -> Dictionary:
|
||||
var path: String = params.get("path", "")
|
||||
var force_reload: bool = params.get("force_reload", false)
|
||||
if path.is_empty():
|
||||
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
|
||||
|
||||
@@ -156,16 +164,91 @@ func open_scene(params: Dictionary) -> Dictionary:
|
||||
if not ResourceLoader.exists(path):
|
||||
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Scene not found: %s" % path)
|
||||
|
||||
EditorInterface.open_scene_from_path(path)
|
||||
|
||||
return {
|
||||
"data": {
|
||||
"path": path,
|
||||
"undoable": false,
|
||||
"reason": "Scene navigation cannot be undone via editor undo",
|
||||
}
|
||||
var scene_root := EditorInterface.get_edited_scene_root()
|
||||
var current_path := scene_root.scene_file_path if scene_root else ""
|
||||
## Instance id of the root at call time. A completed open OR reload always
|
||||
## replaces the edited-scene root with a NEW instance, so this is the
|
||||
## reliable completion signal — unlike scene_file_path, which is unchanged
|
||||
## across a force_reload of the already-open scene (#633 review).
|
||||
var prev_root_id := scene_root.get_instance_id() if scene_root else 0
|
||||
var payload := {
|
||||
"path": path,
|
||||
"force_reload": force_reload,
|
||||
"reloaded_from_disk": false,
|
||||
"previous_scene_path": current_path,
|
||||
"undoable": false,
|
||||
"reason": "Scene navigation cannot be undone via editor undo",
|
||||
}
|
||||
|
||||
if current_path == path and not force_reload:
|
||||
## Already the edited scene — nothing switches, reply immediately.
|
||||
payload["switched"] = true
|
||||
payload["settle"] = "already_current"
|
||||
return {"data": payload}
|
||||
|
||||
if force_reload and current_path == path:
|
||||
EditorInterface.reload_scene_from_path(path)
|
||||
payload["reloaded_from_disk"] = true
|
||||
else:
|
||||
EditorInterface.open_scene_from_path(path)
|
||||
|
||||
## The tab switch completes asynchronously; replying now lets an immediate
|
||||
## follow-up write land on the PREVIOUS scene (#633 — a scene_save issued
|
||||
## right after open_scene saved the old scene). Defer the reply until the
|
||||
## edited scene actually is `path` AND its root is a fresh instance, so
|
||||
## success means "the editor is now editing the (re)loaded scene".
|
||||
var request_id: String = params.get("_request_id", "")
|
||||
if _connection != null and not request_id.is_empty():
|
||||
_finish_open_scene_deferred(_connection, request_id, path, prev_root_id, payload)
|
||||
return McpDispatcher.DEFERRED_RESPONSE
|
||||
|
||||
## Synchronous fallback (batch_execute and unit-test contexts can't await):
|
||||
## preserve the old reply-immediately behavior, flagged as not waited on.
|
||||
payload["switched"] = false
|
||||
payload["settle"] = "not_waited"
|
||||
return {"data": payload}
|
||||
|
||||
|
||||
## `static` is load-bearing (same reason as FilesystemHandler's deferred scan
|
||||
## finish): the coroutine must outlive this RefCounted handler, which can be
|
||||
## freed mid-await by an editor_reload_plugin. Parameterise everything;
|
||||
## reference no instance state.
|
||||
static func _finish_open_scene_deferred(
|
||||
connection: McpConnection,
|
||||
request_id: String,
|
||||
path: String,
|
||||
prev_root_id: int,
|
||||
payload: Dictionary,
|
||||
) -> void:
|
||||
if not is_instance_valid(connection):
|
||||
return
|
||||
var tree := connection.get_tree()
|
||||
if tree == null:
|
||||
return
|
||||
# Hand back a frame so _dispatch() registers this request as deferred
|
||||
# before the coroutine can push a reply.
|
||||
await tree.process_frame
|
||||
var deadline_ms := Time.get_ticks_msec() + _OPEN_SETTLE_MAX_MSEC
|
||||
while Time.get_ticks_msec() < deadline_ms:
|
||||
var root := EditorInterface.get_edited_scene_root()
|
||||
# Require BOTH the target path AND a fresh root instance: a
|
||||
# force_reload keeps scene_file_path == path across the reload, so the
|
||||
# instance swap is what proves the (re)load actually completed rather
|
||||
# than the coroutine settling on the stale pre-reload root.
|
||||
if root != null and root.scene_file_path == path and root.get_instance_id() != prev_root_id:
|
||||
if not is_instance_valid(connection):
|
||||
return
|
||||
payload["switched"] = true
|
||||
payload["settle"] = "settled"
|
||||
connection.send_deferred_response(request_id, {"data": payload})
|
||||
return
|
||||
await tree.process_frame
|
||||
if not is_instance_valid(connection):
|
||||
return
|
||||
payload["switched"] = false
|
||||
payload["settle"] = "timeout"
|
||||
connection.send_deferred_response(request_id, {"data": payload})
|
||||
|
||||
|
||||
## Save the currently edited scene.
|
||||
## Pauses WebSocket processing during save to prevent re-entrant _process()
|
||||
|
||||
Reference in New Issue
Block a user