fix(network): prevent null multiplayer peer crashes offline/teardown and fix Nakama RPC session validation
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user