Files

202 lines
7.6 KiB
GDScript

@tool
extends RefCounted
# editor.reload_filesystem — trigger an editor filesystem rescan.
# Run this after external file changes (created/moved/deleted by tools outside the editor)
# so load() and the FileSystem dock reflect reality.
static func reload_filesystem(_params: Dictionary) -> Dictionary:
EditorInterface.get_resource_filesystem().scan()
return {"data": {"scanned": true}}
# editor.save_all_scenes — save every open edited scene.
static func save_all_scenes(_params: Dictionary) -> Dictionary:
EditorInterface.save_all_scenes()
return {"data": {"saved": true}}
# editor.state — consolidated editor + project status in one call.
# Replaces piecewise scene.current + project.get_setting + etc.
static func state(_params: Dictionary) -> Dictionary:
var version := Engine.get_version_info()
var version_str: String = "%d.%d.%d-%s" % [version.major, version.minor, version.patch, version.status]
var current_scene_path := ""
var current_scene_class := ""
var current_scene_name := ""
var root := EditorInterface.get_edited_scene_root()
if root != null:
current_scene_path = root.scene_file_path
current_scene_class = root.get_class()
current_scene_name = String(root.name)
var open_scenes: Array = []
for s in EditorInterface.get_open_scenes():
open_scenes.append(s)
var project_name := ""
if ProjectSettings.has_setting("application/config/name"):
project_name = str(ProjectSettings.get_setting("application/config/name"))
return {"data": {
"godot_version": version_str,
"project_name": project_name,
"current_scene": {
"path": current_scene_path,
"root_name": current_scene_name,
"root_class": current_scene_class,
"open": root != null,
},
"open_scenes": open_scenes,
"playing_scene": EditorInterface.is_playing_scene(),
"playing_scene_path": EditorInterface.get_playing_scene() if EditorInterface.is_playing_scene() else "",
}}
# editor.selection_get — return the currently-selected nodes in the editor tree dock.
# Useful for "operate on whatever I clicked" workflows.
static func selection_get(_params: Dictionary) -> Dictionary:
var selection := EditorInterface.get_selection()
var root := EditorInterface.get_edited_scene_root()
var out: Array = []
for node in selection.get_selected_nodes():
var path_str := ""
if root != null and (node == root or root.is_ancestor_of(node)):
path_str = String(root.get_path_to(node))
out.append({
"name": String(node.name),
"class": node.get_class(),
"node_path": path_str,
})
return {"data": {"selected": out, "count": out.size()}}
# editor.selection_set — select a specific set of nodes in the editor tree dock.
# Params: {node_paths: ["Player", "Enemy"]}
static func selection_set(params: Dictionary) -> Dictionary:
var paths: Array = params.get("node_paths", [])
var root := EditorInterface.get_edited_scene_root()
if root == null:
return {"error": {"code": -32001, "message": "no scene open"}}
var selection := EditorInterface.get_selection()
selection.clear()
var resolved: Array = []
var missing: Array = []
for p in paths:
var node: Node = root if p == "." else root.get_node_or_null(p)
if node == null:
missing.append(p)
continue
selection.add_node(node)
resolved.append(p)
if not missing.is_empty():
return {"error": {"code": -32001, "message": "nodes not found: %s" % str(missing)}}
return {"data": {"selected": resolved}}
# editor.game_screenshot — capture the viewport of the currently-running game.
# Works when the user has pressed F5 (or equivalent) and a game process is running
# with our runtime bridge autoload registered. Writes a request file, polls for
# the bridge to produce the PNG, returns the path.
#
# Params: {output?: "res://.godot/agent_tools/game.png", timeout_ms?: 5000}
static func game_screenshot(params: Dictionary) -> Dictionary:
var output_path: String = params.get("output", "res://.godot/agent_tools/game_screenshot.png")
var timeout_ms: int = int(params.get("timeout_ms", 5000))
if not EditorInterface.is_playing_scene():
return {"error": {"code": -32001,
"message": "no game running — press F5 (or equivalent) to start the game, then call again. Alternative for arbitrary scenes: run.scene_headless with 'screenshot'."}}
var bridge_dir := "res://.godot/agent_tools/bridge"
if not DirAccess.dir_exists_absolute(bridge_dir):
var derr := DirAccess.make_dir_recursive_absolute(bridge_dir)
if derr != OK:
return {"error": {"code": -32001, "message": "mkdir failed: %d" % derr}}
# Wipe stale output so we can detect when the bridge has written fresh data.
if FileAccess.file_exists(output_path):
@warning_ignore("return_value_discarded")
DirAccess.remove_absolute(output_path)
var request_path := bridge_dir + "/request.json"
var request_id := str(Time.get_ticks_msec())
var req := {
"id": request_id,
"type": "screenshot",
"output": output_path,
}
var f := FileAccess.open(request_path, FileAccess.WRITE)
if f == null:
return {"error": {"code": -32001, "message": "could not write bridge request"}}
f.store_string(JSON.stringify(req))
f.close()
# Poll for response — bridge writes response.json with matching id when done.
var response_path := bridge_dir + "/response.json"
var deadline_usec: int = Time.get_ticks_usec() + timeout_ms * 1000
while Time.get_ticks_usec() < deadline_usec:
if FileAccess.file_exists(response_path):
var rf := FileAccess.open(response_path, FileAccess.READ)
if rf:
var body := rf.get_as_text()
rf.close()
var parsed = JSON.parse_string(body)
if typeof(parsed) == TYPE_DICTIONARY and parsed.get("id", "") == request_id:
@warning_ignore("return_value_discarded")
DirAccess.remove_absolute(response_path)
if parsed.has("error"):
return {"error": {"code": -32001, "message": "bridge: %s" % parsed.error}}
return {"data": {
"path": output_path,
"captured": FileAccess.file_exists(output_path),
}}
OS.delay_msec(50)
# Clean up stale request if bridge never picked it up.
if FileAccess.file_exists(request_path):
@warning_ignore("return_value_discarded")
DirAccess.remove_absolute(request_path)
return {"error": {"code": -32001,
"message": "bridge timed out after %dms — is the running game using the _MCPGameBridge autoload? Re-enable the plugin to re-register it, then press F5 again." % timeout_ms}}
# logs.read — pull recent log lines captured by the game-bridge autoload.
# Params: {clear?: false, max_lines?: 200}
static func logs_read(params: Dictionary) -> Dictionary:
var clear: bool = params.get("clear", false)
var max_lines: int = int(params.get("max_lines", 200))
var logs_path := "res://.godot/agent_tools/bridge/logs.json"
if not FileAccess.file_exists(logs_path):
return {"data": {
"entries": [],
"count": 0,
"note": "no log buffer found — the game may not be running or the autoload isn't initialized. Press F5 and try again.",
}}
var f := FileAccess.open(logs_path, FileAccess.READ)
if f == null:
return {"error": {"code": -32001, "message": "could not read logs"}}
var parsed = JSON.parse_string(f.get_as_text())
f.close()
if typeof(parsed) != TYPE_ARRAY:
parsed = []
var entries: Array = parsed
if entries.size() > max_lines:
entries = entries.slice(entries.size() - max_lines)
if clear:
@warning_ignore("return_value_discarded")
DirAccess.remove_absolute(logs_path)
return {"data": {"entries": entries, "count": entries.size()}}
# logs.clear — drop the log buffer regardless of read state.
static func logs_clear(_params: Dictionary) -> Dictionary:
var logs_path := "res://.godot/agent_tools/bridge/logs.json"
if FileAccess.file_exists(logs_path):
@warning_ignore("return_value_discarded")
DirAccess.remove_absolute(logs_path)
return {"data": {"cleared": true}}