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
+75 -49
View File
@@ -6,13 +6,8 @@ const GAME_HELPER_AUTOLOAD_PATH := "res://addons/godot_ai/runtime/game_helper.gd
## Editor-process Logger subclass — captures parse errors, @tool runtime
## errors, and push_error/push_warning so the LLM can read them via
## `logs_read(source="editor")`. Loaded dynamically because
## `extends Logger` requires Godot 4.5+. The logger script lives in the
## `.gdignore`'d `runtime/loggers/` folder so Godot's editor scan never
## parses it (no "Could not find base class Logger" error on < 4.5), and
## LoggerLoader compiles it from source at runtime only after the
## ClassDB.class_exists("Logger") gate below. See issue #231 / #475.
const LoggerLoader := preload("res://addons/godot_ai/runtime/logger_loader.gd")
## `logs_read(source="editor")`.
const EditorLogger := preload("res://addons/godot_ai/runtime/editor_logger.gd")
## EditorSettings keys used to remember which server process the plugin
## spawned — survives editor restarts, lets a later editor session adopt
@@ -48,6 +43,7 @@ const Telemetry := preload("res://addons/godot_ai/telemetry.gd")
const LogBuffer := preload("res://addons/godot_ai/utils/log_buffer.gd")
const GameLogBuffer := preload("res://addons/godot_ai/utils/game_log_buffer.gd")
const EditorLogBuffer := preload("res://addons/godot_ai/utils/editor_log_buffer.gd")
const SurfacedErrorTracker := preload("res://addons/godot_ai/utils/surfaced_error_tracker.gd")
const Dock := preload("res://addons/godot_ai/mcp_dock.gd")
const DebuggerPlugin := preload("res://addons/godot_ai/debugger/mcp_debugger_plugin.gd")
const ClientConfigurator := preload("res://addons/godot_ai/client_configurator.gd")
@@ -83,6 +79,8 @@ const EnvironmentHandler := preload("res://addons/godot_ai/handlers/environment_
const TextureHandler := preload("res://addons/godot_ai/handlers/texture_handler.gd")
const CurveHandler := preload("res://addons/godot_ai/handlers/curve_handler.gd")
const ControlDrawRecipeHandler := preload("res://addons/godot_ai/handlers/control_draw_recipe_handler.gd")
const TilemapHandler := preload("res://addons/godot_ai/handlers/tilemap_handler.gd")
const TilesetHandler := preload("res://addons/godot_ai/handlers/tileset_handler.gd")
## The Python server writes its own PID here on startup (passed as
## `--pid-file`) and unlinks on clean exit. Deterministic replacement
@@ -144,19 +142,14 @@ const STARTUP_TRACE_COUNTER_NAMES := [
##
## `tests/unit/test_plugin_self_update_safety.py` locks this wording in.
##
## `_editor_logger` is untyped because its script extends Godot 4.5+'s Logger
## class: `logger_loader.gd` compiles it at runtime from on-disk source
## (FileAccess + `GDScript.new()`) past the `ClassDB.class_exists("Logger")`
## gate in `_attach_editor_logger`, so the plugin still parses on 4.4. Null on
## Godot < 4.5 or before `_attach_editor_logger` runs; "attached" state IS
## exactly "non-null".
var _connection
var _dispatcher
var _telemetry
var _log_buffer
var _game_log_buffer
var _editor_log_buffer
var _editor_logger
var _surfaced_error_tracker
var _editor_logger: Logger
var _dock
var _handlers: Array = [] # prevent GC of RefCounted handlers
var _debugger_plugin
@@ -197,11 +190,9 @@ func _enter_tree() -> void:
print("MCP | plugin disabled in headless mode")
return
## Self-update from a pre-loggers/ version leaves the old logger scripts
## orphaned at runtime/*.gd (the runner only writes files in the new ZIP,
## it doesn't prune). Those still `extends Logger` and re-emit the parse
## errors on Godot < 4.5. Delete them once so upgraders match a fresh
## install. No-op on fresh installs and dev checkouts (files absent).
## Self-update extracts over the live addon and doesn't prune files that
## disappeared from the new ZIP. Remove obsolete Logger-loader quarantine
## files/folders once so upgraders match a fresh install.
_cleanup_legacy_logger_scripts()
## Register port overrides before spawn so `http_port()` / `ws_port()`
@@ -211,17 +202,24 @@ func _enter_tree() -> void:
_startup_trace_phase("settings_registered")
_log_buffer = LogBuffer.new()
## Apply the persisted dock "Log" toggle before anything logs through the
## buffer. Without this the choice only took effect after a manual toggle
## and reset to noisy on every editor restart (#626).
_log_buffer.enabled = McpSettings.mcp_logging_enabled()
_start_server()
_startup_trace_phase("server_start")
_game_log_buffer = GameLogBuffer.new()
_editor_log_buffer = EditorLogBuffer.new()
_surfaced_error_tracker = SurfacedErrorTracker.new(_editor_log_buffer, _game_log_buffer)
_attach_editor_logger()
_dispatcher = Dispatcher.new(_log_buffer)
_dispatcher = Dispatcher.new(_log_buffer, _surfaced_error_tracker)
_dispatcher.mcp_logging = _log_buffer.enabled
_startup_trace_phase("core_objects")
_connection = Connection.new()
_connection.log_buffer = _log_buffer
_connection.surfaced_error_tracker = _surfaced_error_tracker
_connection.ws_port = _resolved_ws_port
_connection.connect_blocked = _lifecycle.is_connection_blocked()
_connection.connect_block_reason = _lifecycle.get_status_dict().get("message", "")
@@ -233,19 +231,20 @@ func _enter_tree() -> void:
_telemetry = Telemetry.new(_connection)
_debugger_plugin = DebuggerPlugin.new(_log_buffer, _game_log_buffer)
_debugger_plugin = DebuggerPlugin.new(_log_buffer, _game_log_buffer, _editor_log_buffer, _surfaced_error_tracker)
add_debugger_plugin(_debugger_plugin)
_connection.debugger_plugin = _debugger_plugin
_ensure_game_helper_autoload()
var editor_handler := EditorHandler.new(_log_buffer, _connection, _debugger_plugin, _game_log_buffer, _editor_log_buffer)
var editor_handler := EditorHandler.new(_log_buffer, _connection, _debugger_plugin, _game_log_buffer, _editor_log_buffer, null, _surfaced_error_tracker)
var scene_handler := SceneHandler.new(_connection)
var node_handler := NodeHandler.new(get_undo_redo())
var project_handler := ProjectHandler.new(_connection, _debugger_plugin)
var project_handler := ProjectHandler.new(_connection, _debugger_plugin, _editor_log_buffer)
var client_handler := ClientHandler.new()
var script_handler := ScriptHandler.new(get_undo_redo(), _connection)
var resource_handler := ResourceHandler.new(get_undo_redo(), _connection)
var api_handler := ApiHandler.new()
var filesystem_handler := FilesystemHandler.new()
var filesystem_handler := FilesystemHandler.new(_connection)
var signal_handler := SignalHandler.new(get_undo_redo())
var autoload_handler := AutoloadHandler.new()
var input_handler := InputHandler.new()
@@ -263,7 +262,9 @@ func _enter_tree() -> void:
var texture_handler := TextureHandler.new(get_undo_redo(), _connection)
var curve_handler := CurveHandler.new(get_undo_redo(), _connection)
var control_draw_recipe_handler := ControlDrawRecipeHandler.new(get_undo_redo())
_handlers = [editor_handler, scene_handler, node_handler, project_handler, client_handler, script_handler, resource_handler, api_handler, filesystem_handler, signal_handler, autoload_handler, input_handler, test_handler, batch_handler, ui_handler, theme_handler, animation_handler, material_handler, particle_handler, camera_handler, audio_handler, physics_shape_handler, environment_handler, texture_handler, curve_handler, control_draw_recipe_handler]
var tilemap_handler := TilemapHandler.new(get_undo_redo())
var tileset_handler := TilesetHandler.new()
_handlers = [editor_handler, scene_handler, node_handler, project_handler, client_handler, script_handler, resource_handler, api_handler, filesystem_handler, signal_handler, autoload_handler, input_handler, test_handler, batch_handler, ui_handler, theme_handler, animation_handler, material_handler, particle_handler, camera_handler, audio_handler, physics_shape_handler, environment_handler, texture_handler, curve_handler, control_draw_recipe_handler, tilemap_handler, tileset_handler]
_dispatcher.register("get_editor_state", editor_handler.get_editor_state)
_dispatcher.register("get_scene_tree", scene_handler.get_scene_tree)
@@ -318,6 +319,7 @@ func _enter_tree() -> void:
_dispatcher.register("read_file", filesystem_handler.read_file)
_dispatcher.register("write_file", filesystem_handler.write_file)
_dispatcher.register("reimport", filesystem_handler.reimport)
_dispatcher.register("scan_filesystem", filesystem_handler.scan_filesystem)
_dispatcher.register("list_signals", signal_handler.list_signals)
_dispatcher.register("connect_signal", signal_handler.connect_signal)
_dispatcher.register("disconnect_signal", signal_handler.disconnect_signal)
@@ -326,8 +328,10 @@ func _enter_tree() -> void:
_dispatcher.register("remove_autoload", autoload_handler.remove_autoload)
_dispatcher.register("list_actions", input_handler.list_actions)
_dispatcher.register("add_action", input_handler.add_action)
_dispatcher.register("ensure_action", input_handler.ensure_action)
_dispatcher.register("remove_action", input_handler.remove_action)
_dispatcher.register("bind_event", input_handler.bind_event)
_dispatcher.register("ensure_binding", input_handler.ensure_binding)
_dispatcher.register("run_tests", test_handler.run_tests)
_dispatcher.register("get_test_results", test_handler.get_test_results)
_dispatcher.register("batch_execute", batch_handler.batch_execute)
@@ -393,6 +397,12 @@ func _enter_tree() -> void:
_dispatcher.register(
"control_draw_recipe", control_draw_recipe_handler.control_draw_recipe
)
_dispatcher.register("tilemap_set_cell", tilemap_handler.set_cell)
_dispatcher.register("tilemap_set_cells_rect", tilemap_handler.set_cells_rect)
_dispatcher.register("tilemap_clear", tilemap_handler.clear_layer)
_dispatcher.register("tilemap_get_cells", tilemap_handler.get_used_cells)
_dispatcher.register("tileset_get_atlas_tiles", tileset_handler.get_atlas_tiles)
_dispatcher.register("tileset_get_atlas_image", tileset_handler.get_atlas_image)
_connection.dispatcher = _dispatcher
add_child(_connection)
@@ -489,6 +499,7 @@ func _exit_tree() -> void:
_log_buffer = null
_game_log_buffer = null
_editor_log_buffer = null
_surfaced_error_tracker = null
_stop_server()
## Symmetric with prepare_for_update_reload: the static guard persists
@@ -504,9 +515,7 @@ func _exit_tree() -> void:
## Attach editor_logger.gd as a Godot logger so editor-process script
## errors (parse errors, @tool runtime errors, EditorPlugin errors,
## push_error/push_warning) flow into _editor_log_buffer for
## logs_read(source="editor"). Logger subclassing is 4.5+ only; the
## ClassDB gate keeps the plugin loadable on 4.4 with no-op editor logs
## (the buffer stays empty, logs_read returns no entries).
## logs_read(source="editor").
##
## Limitation called out in the issue: parse errors fired *before* the
## plugin's _enter_tree (e.g. during the editor's initial filesystem
@@ -516,36 +525,53 @@ func _exit_tree() -> void:
## file would re-emit them but at the cost of disrupting the user's
## editing state, so we accept the gap.
func _attach_editor_logger() -> void:
if not (ClassDB.class_exists("Logger") and OS.has_method("add_logger")):
return
var logger_script := LoggerLoader.build(LoggerLoader.EDITOR_LOGGER_PATH)
if logger_script == null:
return
_editor_logger = logger_script.new(_editor_log_buffer)
OS.call("add_logger", _editor_logger)
_editor_logger = EditorLogger.new(_editor_log_buffer)
OS.add_logger(_editor_logger)
## Remove the pre-2.5.8 logger scripts left at runtime/*.gd by a self-update
## (the runner doesn't prune files dropped between versions). They `extends
## Logger` and would re-emit "Could not find base class Logger" parse errors
## on Godot < 4.5 even though the live copies now live in the .gdignore'd
## runtime/loggers/ folder. Idempotent: existence-guarded, so it's a no-op on
## fresh installs and symlinked dev checkouts.
## Remove old Logger-quarantine artifacts left by extract-over-live
## self-update. Idempotent: existence-guarded, so it's a no-op on fresh
## installs and symlinked dev checkouts.
func _cleanup_legacy_logger_scripts() -> void:
var legacy := [
"res://addons/godot_ai/runtime/editor_logger.gd",
"res://addons/godot_ai/runtime/editor_logger.gd.uid",
"res://addons/godot_ai/runtime/game_logger.gd",
"res://addons/godot_ai/runtime/game_logger.gd.uid",
var legacy_files := [
"res://addons/godot_ai/runtime/logger_loader.gd",
"res://addons/godot_ai/runtime/logger_loader.gd.uid",
"res://addons/godot_ai/testing/script_error_capture_loader.gd",
"res://addons/godot_ai/testing/script_error_capture_loader.gd.uid",
]
for res_path in legacy:
for res_path in legacy_files:
if FileAccess.file_exists(res_path):
DirAccess.remove_absolute(ProjectSettings.globalize_path(res_path))
var legacy_dirs := [
"res://addons/godot_ai/runtime/loggers",
"res://addons/godot_ai/testing/loggers",
]
for res_path in legacy_dirs:
var absolute := ProjectSettings.globalize_path(res_path)
if DirAccess.dir_exists_absolute(absolute):
_remove_dir_recursive_absolute(absolute)
static func _remove_dir_recursive_absolute(path: String) -> void:
var dir := DirAccess.open(path)
if dir == null:
return
dir.list_dir_begin()
var name := dir.get_next()
while not name.is_empty():
var child := path.path_join(name)
if dir.current_is_dir():
_remove_dir_recursive_absolute(child)
else:
DirAccess.remove_absolute(child)
name = dir.get_next()
dir.list_dir_end()
DirAccess.remove_absolute(path)
func _detach_editor_logger() -> void:
if _editor_logger != null and OS.has_method("remove_logger"):
OS.call("remove_logger", _editor_logger)
if _editor_logger != null:
OS.remove_logger(_editor_logger)
_editor_logger = null