fix(network): prevent null multiplayer peer crashes offline/teardown and fix Nakama RPC session validation
This commit is contained in:
@@ -14,20 +14,24 @@ var _debugger_plugin: McpDebuggerPlugin
|
||||
var _game_log_buffer: McpGameLogBuffer
|
||||
var _editor_log_buffer: McpEditorLogBuffer
|
||||
var _debugger_errors_root: Node
|
||||
var _debugger_search_root_cache: Node
|
||||
var _surfaced_error_tracker
|
||||
|
||||
|
||||
func _init(log_buffer: McpLogBuffer, connection: McpConnection = null, debugger_plugin: McpDebuggerPlugin = null, game_log_buffer: McpGameLogBuffer = null, editor_log_buffer: McpEditorLogBuffer = null, debugger_errors_root: Node = null) -> void:
|
||||
func _init(log_buffer: McpLogBuffer, connection: McpConnection = null, debugger_plugin: McpDebuggerPlugin = null, game_log_buffer: McpGameLogBuffer = null, editor_log_buffer: McpEditorLogBuffer = null, debugger_errors_root: Node = null, surfaced_error_tracker = null) -> void:
|
||||
_log_buffer = log_buffer
|
||||
_connection = connection
|
||||
_debugger_plugin = debugger_plugin
|
||||
_game_log_buffer = game_log_buffer
|
||||
_editor_log_buffer = editor_log_buffer
|
||||
_debugger_errors_root = debugger_errors_root
|
||||
_surfaced_error_tracker = surfaced_error_tracker
|
||||
if _surfaced_error_tracker == null:
|
||||
_surfaced_error_tracker = McpSurfacedErrorTracker.new(_editor_log_buffer, _game_log_buffer, _debugger_errors_root)
|
||||
|
||||
|
||||
func get_editor_state(_params: Dictionary) -> Dictionary:
|
||||
var scene_root := EditorInterface.get_edited_scene_root()
|
||||
var game_status := _current_game_status()
|
||||
var data := {
|
||||
"godot_version": Engine.get_version_info().get("string", "unknown"),
|
||||
"project_name": ProjectSettings.get_setting("application/config/name", ""),
|
||||
@@ -38,6 +42,9 @@ func get_editor_state(_params: Dictionary) -> Dictionary:
|
||||
## false between Play→Stop cycles. Lets capture-source=game callers
|
||||
## poll for a real ready signal instead of guessing with sleep().
|
||||
"game_capture_ready": _debugger_plugin != null and _debugger_plugin.is_game_capture_ready(),
|
||||
"game_status": game_status,
|
||||
"helper_live": bool(game_status.get("helper_live", false)),
|
||||
"session_active": bool(game_status.get("session_active", false)),
|
||||
}
|
||||
## Half-installed addon tree from a failed self-update rollback. When
|
||||
## non-empty, the agent / dock paint the operator-facing recovery copy
|
||||
@@ -72,6 +79,7 @@ func get_logs(params: Dictionary) -> Dictionary:
|
||||
var include_details: bool = bool(params.get("include_details", false))
|
||||
var has_since_cursor := params.has("since_cursor") and params.get("since_cursor") != null
|
||||
var since_cursor: int = maxi(0, int(params.get("since_cursor", 0)))
|
||||
var since_run_id := "" if params.get("since_run_id", null) == null else str(params.get("since_run_id", ""))
|
||||
if not source in VALID_LOG_SOURCES:
|
||||
return ErrorCodes.make(
|
||||
ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
@@ -82,7 +90,7 @@ func get_logs(params: Dictionary) -> Dictionary:
|
||||
"plugin":
|
||||
return _get_plugin_logs(count, offset)
|
||||
"game":
|
||||
return _get_game_logs(count, offset, include_details)
|
||||
return _get_game_logs(count, offset, include_details, since_run_id)
|
||||
"editor":
|
||||
return _get_editor_logs(count, offset, include_details, has_since_cursor, since_cursor)
|
||||
"all":
|
||||
@@ -90,6 +98,17 @@ func get_logs(params: Dictionary) -> Dictionary:
|
||||
return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Unreachable")
|
||||
|
||||
|
||||
func _current_game_status() -> Dictionary:
|
||||
if _debugger_plugin == null:
|
||||
return McpDebuggerPlugin.with_liveness_flags({
|
||||
"status": "stopped",
|
||||
"active": false,
|
||||
"ready": false,
|
||||
"helper_expected": true,
|
||||
})
|
||||
return _debugger_plugin.get_game_status()
|
||||
|
||||
|
||||
func _get_plugin_logs(count: int, offset: int) -> Dictionary:
|
||||
var all_lines := _log_buffer.get_recent(_log_buffer.total_count())
|
||||
var page: Array[Dictionary] = []
|
||||
@@ -107,7 +126,10 @@ func _get_plugin_logs(count: int, offset: int) -> Dictionary:
|
||||
}
|
||||
|
||||
|
||||
func _get_game_logs(count: int, offset: int, include_details: bool) -> Dictionary:
|
||||
func _get_game_logs(count: int, offset: int, include_details: bool, since_run_id: String = "") -> Dictionary:
|
||||
var game_status := _current_game_status()
|
||||
var helper_live := bool(game_status.get("helper_live", false))
|
||||
var session_active := bool(game_status.get("session_active", false))
|
||||
if _game_log_buffer == null:
|
||||
return {
|
||||
"data": {
|
||||
@@ -117,23 +139,75 @@ func _get_game_logs(count: int, offset: int, include_details: bool) -> Dictionar
|
||||
"returned_count": 0,
|
||||
"offset": offset,
|
||||
"run_id": "",
|
||||
"is_running": false,
|
||||
"current_run_id": "",
|
||||
"is_running": session_active,
|
||||
"helper_live": helper_live,
|
||||
"session_active": session_active,
|
||||
"game_status": game_status,
|
||||
"dropped_count": 0,
|
||||
"stale_run_id": false,
|
||||
}
|
||||
}
|
||||
var page := _entries_for_response(_game_log_buffer.get_range(offset, count), include_details)
|
||||
return {
|
||||
"data": {
|
||||
"source": "game",
|
||||
"lines": page,
|
||||
"total_count": _game_log_buffer.total_count(),
|
||||
"returned_count": page.size(),
|
||||
"offset": offset,
|
||||
"run_id": _game_log_buffer.run_id(),
|
||||
"is_running": EditorInterface.is_playing_scene(),
|
||||
"dropped_count": _game_log_buffer.dropped_count(),
|
||||
}
|
||||
var current_run_id := _game_log_buffer.run_id()
|
||||
var target_run_id := since_run_id if not since_run_id.is_empty() else current_run_id
|
||||
var stale_run_id := not since_run_id.is_empty() and since_run_id != current_run_id
|
||||
var run_page := _game_log_buffer.get_run_page(target_run_id, offset, count)
|
||||
var page := _entries_for_response(run_page.get("entries", []), include_details)
|
||||
var data := {
|
||||
"source": "game",
|
||||
"lines": page,
|
||||
"total_count": int(run_page.get("total_count", 0)),
|
||||
"returned_count": page.size(),
|
||||
"offset": offset,
|
||||
"run_id": target_run_id,
|
||||
"current_run_id": current_run_id,
|
||||
"is_running": session_active,
|
||||
"helper_live": helper_live,
|
||||
"session_active": session_active,
|
||||
"game_status": game_status,
|
||||
"dropped_count": _game_log_buffer.dropped_count(),
|
||||
"stale_run_id": stale_run_id,
|
||||
}
|
||||
_merge_editor_errors_hint(data, game_status)
|
||||
return {"data": data}
|
||||
|
||||
|
||||
## #641: boot-time parse errors happen while autoload scripts compile — before
|
||||
## the game helper's logger attaches via OS.add_logger — so they can NEVER
|
||||
## appear in the game buffer. They surface only through the editor scope
|
||||
## (Errors-tab rows + editor logger). Cross-reference them here so an
|
||||
## empty/clean game log is not mistaken for a clean launch.
|
||||
func _merge_editor_errors_hint(data: Dictionary, game_status: Dictionary) -> void:
|
||||
if _debugger_plugin == null:
|
||||
return
|
||||
## A since_run_id read of a prior run must not carry the CURRENT run's
|
||||
## editor errors — the hint interprets the run being read.
|
||||
if bool(data.get("stale_run_id", false)):
|
||||
return
|
||||
## run_token == 0 means no tracked run ever started this session; the
|
||||
## run-start cursor would be 0 and every retained editor error would be
|
||||
## misattributed to "this run".
|
||||
if int(game_status.get("run_token", 0)) <= 0:
|
||||
return
|
||||
## One-shot read — force the scan so rows that landed after the last
|
||||
## gated scan (and before the deferred timers fire) make the FIRST
|
||||
## logs_read(source='game') response, not just a later one.
|
||||
var errors_info: Dictionary = _debugger_plugin.recent_editor_errors_since(
|
||||
int(game_status.get("editor_log_cursor", 0)), true)
|
||||
if str(errors_info.get("scope", "none")) != "run":
|
||||
return
|
||||
var errors: Array = errors_info.get("errors", [])
|
||||
if errors.is_empty():
|
||||
return
|
||||
data["editor_errors_count"] = errors.size()
|
||||
data["editor_errors_hint"] = (
|
||||
"%d editor-side error%s from this run (first: %s) missing from the game log — boot-time parse/load errors occur before the game helper's logger attaches. Read logs_read(source='editor', include_details=true)."
|
||||
% [errors.size(), "s" if errors.size() != 1 else "", _format_editor_error_summary(errors[0])]
|
||||
)
|
||||
|
||||
|
||||
func _format_editor_error_summary(entry: Dictionary) -> String:
|
||||
return McpSurfacedErrorTracker.format_editor_error_summary(entry)
|
||||
|
||||
|
||||
func _get_editor_logs(count: int, offset: int, include_details: bool, has_since_cursor: bool = false, since_cursor: int = 0) -> Dictionary:
|
||||
@@ -211,21 +285,24 @@ func _get_all_logs(count: int, offset: int, include_details: bool) -> Dictionary
|
||||
combined.append({"source": "plugin", "level": "info", "text": line})
|
||||
for entry in _collect_editor_log_entries():
|
||||
combined.append(entry)
|
||||
var run_id := ""
|
||||
var current_run_id := ""
|
||||
var dropped := 0
|
||||
if _game_log_buffer != null:
|
||||
for entry in _game_log_buffer.get_range(0, _game_log_buffer.total_count()):
|
||||
run_id = _game_log_buffer.run_id()
|
||||
current_run_id = run_id
|
||||
dropped = _game_log_buffer.dropped_count()
|
||||
var run_page := _game_log_buffer.get_run_page(run_id, 0, McpGameLogBuffer.MAX_LINES)
|
||||
for entry in run_page.get("entries", []):
|
||||
combined.append(entry)
|
||||
var stop := mini(combined.size(), offset + count)
|
||||
var page: Array[Dictionary] = []
|
||||
for i in range(mini(offset, combined.size()), stop):
|
||||
page.append(combined[i])
|
||||
page = _entries_for_response(page, include_details)
|
||||
var run_id := ""
|
||||
var dropped := 0
|
||||
if _game_log_buffer != null:
|
||||
run_id = _game_log_buffer.run_id()
|
||||
dropped = _game_log_buffer.dropped_count()
|
||||
if _editor_log_buffer != null:
|
||||
dropped += _editor_log_buffer.dropped_count()
|
||||
var game_status := _current_game_status()
|
||||
return {
|
||||
"data": {
|
||||
"source": "all",
|
||||
@@ -234,7 +311,11 @@ func _get_all_logs(count: int, offset: int, include_details: bool) -> Dictionary
|
||||
"returned_count": page.size(),
|
||||
"offset": offset,
|
||||
"run_id": run_id,
|
||||
"is_running": EditorInterface.is_playing_scene(),
|
||||
"current_run_id": current_run_id,
|
||||
"is_running": bool(game_status.get("session_active", false)),
|
||||
"helper_live": bool(game_status.get("helper_live", false)),
|
||||
"session_active": bool(game_status.get("session_active", false)),
|
||||
"game_status": game_status,
|
||||
"dropped_count": dropped,
|
||||
}
|
||||
}
|
||||
@@ -256,208 +337,7 @@ func _entries_for_response(entries: Array[Dictionary], include_details: bool) ->
|
||||
|
||||
|
||||
func _collect_editor_log_entries() -> Array[Dictionary]:
|
||||
var entries: Array[Dictionary] = []
|
||||
if _editor_log_buffer != null:
|
||||
for entry in _editor_log_buffer.get_range(0, _editor_log_buffer.total_count()):
|
||||
entries.append(entry)
|
||||
for entry in _read_debugger_error_entries():
|
||||
if not _has_equivalent_log_entry(entries, entry):
|
||||
entries.append(entry)
|
||||
return entries
|
||||
|
||||
|
||||
func _read_debugger_error_entries() -> Array[Dictionary]:
|
||||
var entries: Array[Dictionary] = []
|
||||
for tree in _locate_debugger_error_trees():
|
||||
for entry in _entries_from_debugger_error_tree(tree):
|
||||
if not _has_equivalent_log_entry(entries, entry):
|
||||
entries.append(entry)
|
||||
return entries
|
||||
|
||||
|
||||
func _locate_debugger_error_trees() -> Array[Tree]:
|
||||
var trees: Array[Tree] = []
|
||||
if _debugger_plugin == null and _debugger_errors_root == null:
|
||||
return trees
|
||||
var root: Node = _debugger_errors_root
|
||||
if root == null:
|
||||
root = _debugger_search_root()
|
||||
if root == null:
|
||||
return trees
|
||||
_collect_debugger_error_trees(root, trees)
|
||||
return trees
|
||||
|
||||
|
||||
func _debugger_search_root() -> Node:
|
||||
## logs_read is a polling tool, so per-call discovery must not recurse the
|
||||
## entire editor UI. EditorDebuggerNode is the bottom-panel container that
|
||||
## owns every ScriptEditorDebugger session tab and lives for the editor's
|
||||
## lifetime — find it once from the base control, then scan only its
|
||||
## subtree on later calls. The error Trees themselves can't be cached:
|
||||
## they are identified by their content, and an emptied tree is
|
||||
## indistinguishable from any other Tree.
|
||||
if is_instance_valid(_debugger_search_root_cache):
|
||||
return _debugger_search_root_cache
|
||||
_debugger_search_root_cache = null
|
||||
var base := EditorInterface.get_base_control()
|
||||
if base == null:
|
||||
return null
|
||||
_debugger_search_root_cache = _find_first_of_class(base, "EditorDebuggerNode")
|
||||
if _debugger_search_root_cache == null:
|
||||
return base
|
||||
return _debugger_search_root_cache
|
||||
|
||||
|
||||
static func _find_first_of_class(node: Node, klass: String) -> Node:
|
||||
if node.get_class() == klass:
|
||||
return node
|
||||
for child in node.get_children():
|
||||
var found := _find_first_of_class(child, klass)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
static func _collect_debugger_error_trees(node: Node, out: Array[Tree]) -> void:
|
||||
if node is Tree and _tree_has_debugger_errors(node as Tree):
|
||||
out.append(node as Tree)
|
||||
for child in node.get_children():
|
||||
if child is Node:
|
||||
_collect_debugger_error_trees(child as Node, out)
|
||||
|
||||
|
||||
static func _tree_has_debugger_errors(tree: Tree) -> bool:
|
||||
var root := tree.get_root()
|
||||
if root == null:
|
||||
return false
|
||||
var item := root.get_first_child()
|
||||
while item != null:
|
||||
if _is_debugger_error_item(item):
|
||||
return true
|
||||
item = item.get_next()
|
||||
return false
|
||||
|
||||
|
||||
static func _entries_from_debugger_error_tree(tree: Tree) -> Array[Dictionary]:
|
||||
var entries: Array[Dictionary] = []
|
||||
var root := tree.get_root()
|
||||
if root == null:
|
||||
return entries
|
||||
var item := root.get_first_child()
|
||||
while item != null:
|
||||
if _is_debugger_error_item(item):
|
||||
entries.append(_entry_from_debugger_error_item(item))
|
||||
item = item.get_next()
|
||||
return entries
|
||||
|
||||
|
||||
static func _entry_from_debugger_error_item(item: TreeItem) -> Dictionary:
|
||||
var title := item.get_text(1)
|
||||
var loc := _location_from_metadata(item.get_metadata(0))
|
||||
var function := _function_from_title(title)
|
||||
return {
|
||||
"source": "editor",
|
||||
"level": "warn" if item.has_meta("_is_warning") else "error",
|
||||
"text": title,
|
||||
"path": str(loc.get("path", "")),
|
||||
"line": int(loc.get("line", 0)),
|
||||
"function": function,
|
||||
"details": _details_from_debugger_error_item(item, loc, function),
|
||||
}
|
||||
|
||||
|
||||
static func _details_from_debugger_error_item(item: TreeItem, loc: Dictionary, function: String) -> Dictionary:
|
||||
var children: Array[Dictionary] = []
|
||||
var child := item.get_first_child()
|
||||
while child != null:
|
||||
var child_loc := _location_from_metadata(child.get_metadata(0))
|
||||
children.append({
|
||||
"label": child.get_text(0),
|
||||
"text": child.get_text(1),
|
||||
"path": str(child_loc.get("path", "")),
|
||||
"line": int(child_loc.get("line", 0)),
|
||||
})
|
||||
child = child.get_next()
|
||||
return {
|
||||
"debugger_tab": "Errors",
|
||||
"time": item.get_text(0),
|
||||
"message": item.get_text(1),
|
||||
"error_type_name": "warning" if item.has_meta("_is_warning") else "error",
|
||||
"source": {
|
||||
"path": str(loc.get("path", "")),
|
||||
"line": int(loc.get("line", 0)),
|
||||
"function": function,
|
||||
},
|
||||
"resolved": {
|
||||
"path": str(loc.get("path", "")),
|
||||
"line": int(loc.get("line", 0)),
|
||||
"function": function,
|
||||
},
|
||||
"children": children,
|
||||
"frames": _frames_from_error_children(children),
|
||||
}
|
||||
|
||||
|
||||
static func _is_debugger_error_item(item: TreeItem) -> bool:
|
||||
return item.has_meta("_is_warning") or item.has_meta("_is_error")
|
||||
|
||||
|
||||
## ScriptEditorDebugger lays out an error item's children flat, in order: an
|
||||
## optional "<X Error>" row, one "<X Source>" row, then one row per stack
|
||||
## frame. Only frame 0 carries the "<Stack Trace>" label (TTR-translated);
|
||||
## later frames have an empty label. Every frame row carries [path, line]
|
||||
## metadata, but so can the Error/Source rows, so metadata alone can't
|
||||
## identify frames — the frame run has to be found first.
|
||||
static func _frames_from_error_children(children: Array[Dictionary]) -> Array[Dictionary]:
|
||||
var start := -1
|
||||
for i in children.size():
|
||||
if str(children[i].label).contains("Stack Trace"):
|
||||
start = i
|
||||
break
|
||||
if start < 0:
|
||||
## Non-English editor locale: the "<Stack Trace>" label is translated.
|
||||
## Frames past the first are the only rows with an empty label and a
|
||||
## real location; back up one row to recover the labeled first frame
|
||||
## (rows before the frame run always have a non-empty label).
|
||||
for i in children.size():
|
||||
if str(children[i].label).is_empty() and not str(children[i].path).is_empty():
|
||||
start = maxi(i - 1, 0)
|
||||
break
|
||||
if start < 0:
|
||||
return []
|
||||
var frames: Array[Dictionary] = []
|
||||
for i in range(start, children.size()):
|
||||
if str(children[i].path).is_empty():
|
||||
continue
|
||||
frames.append({
|
||||
"path": children[i].path,
|
||||
"line": children[i].line,
|
||||
"function": _function_from_frame_text(children[i].text),
|
||||
})
|
||||
return frames
|
||||
|
||||
|
||||
static func _location_from_metadata(meta: Variant) -> Dictionary:
|
||||
if meta is Array and meta.size() >= 2:
|
||||
return {"path": str(meta[0]), "line": int(meta[1])}
|
||||
return {"path": "", "line": 0}
|
||||
|
||||
|
||||
static func _function_from_title(title: String) -> String:
|
||||
var colon := title.find(": ")
|
||||
if colon <= 0:
|
||||
return ""
|
||||
return title.substr(0, colon)
|
||||
|
||||
|
||||
static func _function_from_frame_text(text: String) -> String:
|
||||
var marker := text.find(" @ ")
|
||||
if marker < 0:
|
||||
return ""
|
||||
var fn := text.substr(marker + 3).strip_edges()
|
||||
if fn.ends_with("()"):
|
||||
fn = fn.substr(0, fn.length() - 2)
|
||||
return fn
|
||||
return _surfaced_error_tracker.collect_editor_log_entries()
|
||||
|
||||
|
||||
static func _slice_entries(entries: Array[Dictionary], offset: int, count: int) -> Array[Dictionary]:
|
||||
@@ -468,23 +348,6 @@ static func _slice_entries(entries: Array[Dictionary], offset: int, count: int)
|
||||
return page
|
||||
|
||||
|
||||
static func _has_equivalent_log_entry(entries: Array[Dictionary], candidate: Dictionary) -> bool:
|
||||
var key := _log_entry_key(candidate)
|
||||
for entry in entries:
|
||||
if _log_entry_key(entry) == key:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
static func _log_entry_key(entry: Dictionary) -> String:
|
||||
return "%s|%s|%s|%s" % [
|
||||
str(entry.get("level", "")),
|
||||
str(entry.get("text", "")),
|
||||
str(entry.get("path", "")),
|
||||
str(entry.get("line", 0)),
|
||||
]
|
||||
|
||||
|
||||
## Map of human-readable monitor names to Performance.Monitor enum values.
|
||||
const MONITORS := {
|
||||
"time/fps": Performance.TIME_FPS,
|
||||
@@ -591,8 +454,30 @@ func take_screenshot(params: Dictionary) -> Dictionary:
|
||||
return McpDispatcher.DEFERRED_RESPONSE
|
||||
"cinematic":
|
||||
return _take_cinematic_screenshot(max_resolution)
|
||||
"viewport_2d":
|
||||
viewport = EditorInterface.get_editor_viewport_2d()
|
||||
if viewport == null:
|
||||
return ErrorCodes.make(ErrorCodes.EDITOR_NOT_READY, "No 2D viewport available")
|
||||
var scene_root_2d := EditorInterface.get_edited_scene_root()
|
||||
if scene_root_2d == null:
|
||||
return ErrorCodes.make(ErrorCodes.EDITOR_NOT_READY,
|
||||
"No scene open — open a scene first")
|
||||
if not view_target.is_empty() or coverage or custom_elevation != null or custom_azimuth != null or custom_fov != null:
|
||||
return ErrorCodes.make(
|
||||
ErrorCodes.INVALID_PARAMS,
|
||||
"view_target, coverage, elevation, azimuth, and fov are not supported with source='viewport_2d'"
|
||||
)
|
||||
## Capture the 2D editor viewport directly; no view_target/coverage for 2D.
|
||||
RenderingServer.force_draw(false)
|
||||
var image_2d: Image = viewport.get_texture().get_image()
|
||||
if image_2d == null or image_2d.is_empty():
|
||||
return _empty_image_error(
|
||||
"viewport_2d",
|
||||
"Captured an empty image from the 2D viewport. The 2D viewport produced no output — typically headless mode or the 2D viewport has not drawn a frame yet."
|
||||
)
|
||||
return _finalize_image(image_2d, "viewport_2d", max_resolution)
|
||||
_:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid source '%s' — use 'viewport', 'cinematic', or 'game'" % source)
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid source '%s' — use 'viewport', 'viewport_2d', 'cinematic', or 'game'" % source)
|
||||
|
||||
## Handle view_target: temporarily reposition the editor's own camera to
|
||||
## frame one or more target nodes, force a render, capture, then restore.
|
||||
@@ -845,12 +730,14 @@ static func viewport_screenshot_precheck(scene_root: Node) -> Dictionary:
|
||||
return {}
|
||||
var root_type := scene_root.get_class()
|
||||
var hint: String
|
||||
if scene_root is Node2D or scene_root is Control:
|
||||
var is_2d_scene := scene_root is CanvasItem
|
||||
if is_2d_scene:
|
||||
hint = (
|
||||
"The 3D viewport is empty because the current scene is 2D (%s root) with no Node3D descendants. "
|
||||
+ "Options: (a) open a 3D scene, "
|
||||
+ "(b) use source=\"cinematic\" if a Camera3D exists in the scene, "
|
||||
+ "(c) call scene_get_hierarchy first to inspect what's available."
|
||||
+ "(c) use source=\"viewport_2d\" to capture the 2D editor viewport directly, "
|
||||
+ "(d) call scene_get_hierarchy first to inspect what's available."
|
||||
) % root_type
|
||||
else:
|
||||
hint = (
|
||||
@@ -859,7 +746,10 @@ static func viewport_screenshot_precheck(scene_root: Node) -> Dictionary:
|
||||
+ "(b) use source=\"cinematic\" if a Camera3D exists in the scene, "
|
||||
+ "(c) call scene_get_hierarchy first to inspect what's available."
|
||||
) % root_type
|
||||
return _make_viewport_not_3d_error(root_type, hint)
|
||||
var err := _make_viewport_not_3d_error(root_type, hint)
|
||||
if is_2d_scene:
|
||||
err["error"]["data"]["suggestion"] = "use source='viewport_2d' for 2D scenes"
|
||||
return err
|
||||
|
||||
|
||||
## True if scene_root is itself a Node3D or owns any Node3D descendant.
|
||||
@@ -1029,37 +919,7 @@ func clear_logs(params: Dictionary) -> Dictionary:
|
||||
|
||||
|
||||
func _clear_debugger_error_trees() -> int:
|
||||
var cleared := 0
|
||||
for tree in _locate_debugger_error_trees():
|
||||
cleared += _entries_from_debugger_error_tree(tree).size()
|
||||
if not _press_debugger_clear_button(tree):
|
||||
## No Clear button near this tree (synthetic roots in tests).
|
||||
## A raw clear is acceptable there; the real panel always routes
|
||||
## through the button below.
|
||||
tree.clear()
|
||||
return cleared
|
||||
|
||||
|
||||
## Clear via ScriptEditorDebugger's own Clear button so the engine runs
|
||||
## _clear_errors_list() — clearing the Tree directly leaves error_count/
|
||||
## warning_count, the "Errors (N)" tab badge, the errors_cleared signal, and
|
||||
## the toolbar button states out of sync with the emptied tree. The button is
|
||||
## identified by its pressed-connection target, not its (translated) label.
|
||||
static func _press_debugger_clear_button(tree: Tree) -> bool:
|
||||
var parent := tree.get_parent()
|
||||
if parent == null:
|
||||
return false
|
||||
var stack: Array[Node] = [parent]
|
||||
while not stack.is_empty():
|
||||
var node: Node = stack.pop_back()
|
||||
if node is BaseButton:
|
||||
for conn in node.get_signal_connection_list("pressed"):
|
||||
if str(conn.get("callable", "")).contains("_clear_errors_list"):
|
||||
node.emit_signal("pressed")
|
||||
return true
|
||||
for child in node.get_children():
|
||||
stack.push_back(child)
|
||||
return false
|
||||
return _surfaced_error_tracker.clear_debugger_error_trees()
|
||||
|
||||
|
||||
func reload_plugin(_params: Dictionary) -> Dictionary:
|
||||
|
||||
Reference in New Issue
Block a user