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
+74 -10
View File
@@ -3,7 +3,7 @@ extends RefCounted
const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
const DiagnosticsCapture := preload("res://addons/godot_ai/utils/diagnostics_capture.gd")
const LoggerLoader := preload("res://addons/godot_ai/runtime/logger_loader.gd")
const ValidationLogger := preload("res://addons/godot_ai/runtime/validation_logger.gd")
## Handles script creation, reading, attaching, detaching, and symbol inspection.
@@ -63,6 +63,33 @@ func create_script(params: Dictionary) -> Dictionary:
}
_attach_gdscript_diagnostics(data, path, content)
# A freshly-declared `class_name` is NOT in the global class table until a
# filesystem scan runs — update_file() below registers the file with the
# resource pipeline but not the class registry (see the scan() comment).
# Surface that precisely (only when the class isn't already registered) so a
# headless caller knows to follow up with filesystem_manage(op="scan")
# instead of hitting a confusing "Unknown type" / "Unknown resource type" on
# the very next call. We don't scan here — a scan() per create is the exact
# SIGABRT race documented below; the explicit op is single-flight.
# Skip the hint when the script failed to parse: a scan won't register a
# class from a broken script, so pointing at op="scan" would steer the caller
# away from the real fix (the parse error already attached above).
var declared_class := _extract_class_name(content)
if (
not declared_class.is_empty()
and not _script_has_error_diagnostics(data)
and not _class_name_registered(declared_class)
):
data["class_name"] = declared_class
data["class_registration"] = "scan_required"
data["class_registration_hint"] = (
"New class_name '%s' isn't in the global class table yet. " % declared_class
+ "Call filesystem_manage(op=\"scan\") if it won't resolve on the next "
+ "call (e.g. resource_manage op=\"create\", or used as a type in another "
+ "script). The editor also registers it on its next filesystem scan or "
+ "when its window regains focus."
)
# Register just this file with the editor instead of a full recursive
# scan(). A scan() per write stacks `update_scripts_classes` /
# `update_script_paths_documentation` WorkerThreadPool tasks under concurrent
@@ -142,6 +169,48 @@ static func _finish_create_script_deferred(
connection.send_deferred_response(request_id, {"data": payload})
## Extract the `class_name` a script declares, or "" if none. A cheap line scan
## (no full parse) for create_script's "scan_required" hint. Stops at the first
## space/tab or comma so all three valid forms yield just the name:
## `class_name Foo`, `class_name Foo extends Bar`, and the icon form
## `class_name Foo, "res://icon.svg"`.
static func _extract_class_name(content: String) -> String:
for raw_line in content.split("\n"):
var line := raw_line.strip_edges()
if line.begins_with("class_name "):
var rest := line.substr(11).strip_edges()
var cut := rest.length()
for i in rest.length():
var ch := rest[i]
if ch == " " or ch == "\t" or ch == ",":
cut = i
break
return rest.substr(0, cut)
return ""
## True if create_script's diagnostics captured a parse error for this script.
## Used to suppress the "scan_required" hint when the class can't register
## anyway — see create_script.
static func _script_has_error_diagnostics(data: Dictionary) -> bool:
for diag in data.get("diagnostics", []):
if diag is Dictionary and diag.get("level", "") == "error":
return true
return false
## True if `cn` is already usable as a type — an engine built-in (ClassDB) or an
## already-registered project global class. A brand-new class_name returns false
## until a filesystem scan registers it.
static func _class_name_registered(cn: String) -> bool:
if ClassDB.class_exists(cn):
return true
for entry in ProjectSettings.get_global_class_list():
if entry.get("class", "") == cn:
return true
return false
func read_script(params: Dictionary) -> Dictionary:
var path: String = params.get("path", "")
@@ -204,21 +273,16 @@ static func _validate_gdscript_source(content: String) -> Dictionary:
}
static func _capture_gdscript_load_diagnostics(path: String) -> Dictionary:
if not (ClassDB.class_exists("Logger") and OS.has_method("add_logger") and OS.has_method("remove_logger")):
return _empty_diagnostics_capture()
var logger_script := LoggerLoader.build(LoggerLoader.VALIDATION_LOGGER_PATH)
if logger_script == null:
return _empty_diagnostics_capture()
func _capture_gdscript_load_diagnostics(path: String) -> Dictionary:
var buffer := McpEditorLogBuffer.new()
var logger = logger_script.new(buffer)
var logger := ValidationLogger.new(buffer)
var capture := DiagnosticsCapture.capture_this_file(buffer, path, func() -> Dictionary:
OS.call("add_logger", logger)
OS.add_logger(logger)
# ResourceLoader.load() reports parse failure instead of throwing, and
# a failed GDScript parse does not execute user code; remove immediately
# after the synchronous load to keep the private capture window tiny.
ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
OS.call("remove_logger", logger)
OS.remove_logger(logger)
return {}
)
return capture