@beremaran/godot-agent-loop 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +925 -0
- package/addons/godot_agent_loop/LICENSE +23 -0
- package/addons/godot_agent_loop/README.md +31 -0
- package/addons/godot_agent_loop/plugin.cfg +8 -0
- package/addons/godot_agent_loop/plugin.gd +417 -0
- package/agent-plugin/.claude-plugin/plugin.json +19 -0
- package/agent-plugin/.codex-plugin/plugin.json +37 -0
- package/agent-plugin/.mcp.json +14 -0
- package/agent-plugin/adapter-manifest.json +45 -0
- package/agent-plugin/pi/extension.ts +136 -0
- package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
- package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
- package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
- package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
- package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
- package/build/authoring-session-manager.js +237 -0
- package/build/domain-tool-registries.js +207 -0
- package/build/editor-bridge-protocol.js +1 -0
- package/build/editor-connection.js +112 -0
- package/build/editor-mutation-guard.js +30 -0
- package/build/editor-plugin-installer.js +220 -0
- package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
- package/build/game-command-service.js +49 -0
- package/build/game-connection.js +337 -0
- package/build/godot-executable.js +156 -0
- package/build/godot-process-manager.js +112 -0
- package/build/godot-subprocess.js +23 -0
- package/build/headless-operation-runner.js +68 -0
- package/build/headless-operation-service.js +65 -0
- package/build/index.js +478 -0
- package/build/interaction-server-installer.js +234 -0
- package/build/opencode-setup.js +199 -0
- package/build/project-support.js +219 -0
- package/build/runtime-protocol.js +202 -0
- package/build/scripts/godot_operations.gd +1534 -0
- package/build/scripts/mcp_editor_plugin.gd +417 -0
- package/build/scripts/mcp_interaction_server.gd +1255 -0
- package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
- package/build/scripts/mcp_runtime/command_params.gd +339 -0
- package/build/scripts/mcp_runtime/core_domain.gd +591 -0
- package/build/scripts/mcp_runtime/input_domain.gd +695 -0
- package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
- package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
- package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
- package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
- package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
- package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
- package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
- package/build/scripts/mcp_runtime/system_domain.gd +277 -0
- package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
- package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
- package/build/scripts/validate_script.gd +28 -0
- package/build/server-instructions.js +11 -0
- package/build/session-timing.js +20 -0
- package/build/tool-argument-validation.js +120 -0
- package/build/tool-definitions.js +2727 -0
- package/build/tool-handlers/game-tool-handlers.js +1345 -0
- package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
- package/build/tool-handlers/project-handler-services.js +1458 -0
- package/build/tool-handlers/project-tool-handlers.js +1506 -0
- package/build/tool-handlers/visual-regression-service.js +139 -0
- package/build/tool-manifest.js +1193 -0
- package/build/tool-mutation-policy.js +122 -0
- package/build/tool-registry.js +34 -0
- package/build/tool-surface.js +70 -0
- package/build/utils.js +273 -0
- package/package.json +110 -0
- package/product.json +52 -0
- package/scripts/prepare-package.js +42 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
extends Node
|
|
2
|
+
|
|
3
|
+
# Base class for a runtime command domain.
|
|
4
|
+
#
|
|
5
|
+
# A domain owns the handlers for one Godot subsystem plus any state those
|
|
6
|
+
# handlers keep (held keys, debug-draw objects, sockets), and registers them
|
|
7
|
+
# with the server's command registry. Domains are children of the interaction
|
|
8
|
+
# server node, so get_tree() and get_viewport() resolve exactly as they did
|
|
9
|
+
# when the handlers lived on the server itself.
|
|
10
|
+
#
|
|
11
|
+
# This class is the only place domain code touches the server: handlers call
|
|
12
|
+
# the helpers below and never see sessions, sockets, request IDs, or the
|
|
13
|
+
# registry. That keeps the transport layer free of subsystem knowledge and
|
|
14
|
+
# lets a domain move without changing the protocol implementation.
|
|
15
|
+
|
|
16
|
+
const CommandParams = preload("res://mcp_runtime/command_params.gd")
|
|
17
|
+
const VariantCodec = preload("res://mcp_runtime/variant_codec.gd")
|
|
18
|
+
# The composition root is always the autoload at res://mcp_interaction_server.gd.
|
|
19
|
+
# It loads its domains by path at runtime rather than preloading them, so naming
|
|
20
|
+
# its script here types every transport helper below without a preload cycle.
|
|
21
|
+
const InteractionServer = preload("res://mcp_interaction_server.gd")
|
|
22
|
+
|
|
23
|
+
var _server: InteractionServer
|
|
24
|
+
var _registrar: Callable
|
|
25
|
+
var _codec: VariantCodec
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Called by the composition root before register_commands(). `registrar` is the
|
|
29
|
+
# server's _register_command(command, handler) callable, which is what decides
|
|
30
|
+
# cancellability from the transport's declarative CANCELLABLE_COMMANDS list.
|
|
31
|
+
func setup(server: InteractionServer, registrar: Callable) -> void:
|
|
32
|
+
_server = server
|
|
33
|
+
_registrar = registrar
|
|
34
|
+
_codec = server._codec
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Overridden by each domain to register its commands via register_command().
|
|
38
|
+
func register_commands() -> void:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
func register_command(command: String, handler: Callable) -> void:
|
|
43
|
+
_registrar.call(command, handler)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# --- Transport helpers ---
|
|
47
|
+
# Mirrors of the server-side helpers, so a handler body reads the same whether
|
|
48
|
+
# it lives in a domain or in the composition root.
|
|
49
|
+
|
|
50
|
+
# Completes the active request. A result dictionary is sent as the JSON-RPC
|
|
51
|
+
# result; an {"error": ...} dictionary becomes a standardized command failure.
|
|
52
|
+
func respond(result: Dictionary) -> void:
|
|
53
|
+
_server._send_response(result)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Sends the standardized -32000 failure and reports whether the handler must
|
|
57
|
+
# stop. Every handler calls this once after reading its parameters.
|
|
58
|
+
func params_invalid(reader: CommandParams) -> bool:
|
|
59
|
+
var invalid: bool = _server._params_invalid(reader)
|
|
60
|
+
return invalid
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# The same failure for a handler that failed the reader itself, on a rule the
|
|
64
|
+
# accessors cannot express, and is about to stop.
|
|
65
|
+
func send_params_error(reader: CommandParams) -> void:
|
|
66
|
+
_server._send_params_error(reader)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
func require_node(reader: CommandParams, param_name: String = "node_path", default_path: String = "") -> Node:
|
|
70
|
+
var node: Node = _server._require_node(reader, param_name, default_path)
|
|
71
|
+
return node
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
func godot_error_data(err: int) -> Dictionary:
|
|
75
|
+
var data: Dictionary = _server._godot_error_data(err)
|
|
76
|
+
return data
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# JSON-safe encoding and typed decoding are owned by the shared codec service.
|
|
80
|
+
func variant_to_json(value: Variant) -> Variant:
|
|
81
|
+
_codec.configure(_server.max_json_nesting_depth, _server.max_json_collection_items)
|
|
82
|
+
var encoded: Variant = _codec.encode(value)
|
|
83
|
+
return encoded
|
|
84
|
+
|
|
85
|
+
func json_to_variant(value: Variant, type_hint: String = "") -> Variant:
|
|
86
|
+
_codec.configure(_server.max_json_nesting_depth, _server.max_json_collection_items)
|
|
87
|
+
return _codec.decode(value, type_hint)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
func json_to_variant_for_property(node: Node, property: String, value: Variant) -> Variant:
|
|
91
|
+
_codec.configure(_server.max_json_nesting_depth, _server.max_json_collection_items)
|
|
92
|
+
return _codec.decode_for_property(node, property, value)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Async signal waits query cancellation and report timeout through transport
|
|
96
|
+
# helpers without gaining access to sessions or request IDs.
|
|
97
|
+
func cancellation_requested() -> bool:
|
|
98
|
+
return _server._is_active_request_cancelled()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
func respond_timeout(message: String, details: Dictionary = {}) -> void:
|
|
102
|
+
_server._send_timeout_response(message, details)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# A capability the active engine build cannot provide (rather than a bad request).
|
|
106
|
+
# Returning this keeps the caller from believing a silently-dropped write landed.
|
|
107
|
+
func respond_limit(message: String, details: Dictionary = {}) -> void:
|
|
108
|
+
_server._send_limit_response(message, details)
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
extends "res://mcp_runtime/runtime_domain.gd"
|
|
2
|
+
|
|
3
|
+
# 2D domain: TileMapLayer cells, canvas layers/modulate, immediate-mode canvas
|
|
4
|
+
# drawing, 2D lights and occluders, parallax backgrounds, Line2D/Polygon2D
|
|
5
|
+
# points, and Path2D curves. Owns the canvas-draw node and its command list.
|
|
6
|
+
|
|
7
|
+
# The draw node is created lazily on first draw and re-created if it was freed
|
|
8
|
+
# with the scene; the accumulated draw commands survive scene changes.
|
|
9
|
+
var _canvas_draw_node: Node2D = null
|
|
10
|
+
var _draw_commands: Array = []
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# The draw node is parented into the scene, not under this domain, so it would
|
|
14
|
+
# outlive the server. Free it with the domain that owns it.
|
|
15
|
+
func _exit_tree() -> void:
|
|
16
|
+
_draw_commands.clear()
|
|
17
|
+
if _canvas_draw_node != null and is_instance_valid(_canvas_draw_node):
|
|
18
|
+
_canvas_draw_node.queue_free()
|
|
19
|
+
_canvas_draw_node = null
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
func register_commands() -> void:
|
|
23
|
+
register_command("tilemap", _cmd_tilemap)
|
|
24
|
+
register_command("canvas", _cmd_canvas)
|
|
25
|
+
register_command("canvas_draw", _cmd_canvas_draw)
|
|
26
|
+
register_command("light_2d", _cmd_light_2d)
|
|
27
|
+
register_command("parallax", _cmd_parallax)
|
|
28
|
+
register_command("shape_2d", _cmd_shape_2d)
|
|
29
|
+
register_command("path_2d", _cmd_path_2d)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Records a structured failure when the resolved node is not the class the
|
|
33
|
+
# command drives; the caller still routes through params_invalid().
|
|
34
|
+
func _require_class(reader: CommandParams, node: Node, type_name: String) -> void:
|
|
35
|
+
if reader.failed() or node == null:
|
|
36
|
+
return
|
|
37
|
+
reader.fail("Node is not a %s: %s" % [type_name, node.get_class()],
|
|
38
|
+
{"param": "node_path", "reason": "invalid_value", "expected": type_name, "value": node.get_class()})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
func _color_from(value: Dictionary) -> Color:
|
|
42
|
+
return Color(
|
|
43
|
+
CommandParams.json_float(value, "r", 1.0),
|
|
44
|
+
CommandParams.json_float(value, "g", 1.0),
|
|
45
|
+
CommandParams.json_float(value, "b", 1.0),
|
|
46
|
+
CommandParams.json_float(value, "a", 1.0),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
func _vector2_from(value: Dictionary, default_x: float = 0.0, default_y: float = 0.0) -> Vector2:
|
|
51
|
+
return Vector2(CommandParams.json_float(value, "x", default_x), CommandParams.json_float(value, "y", default_y))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
func _apply_optional_name(reader: CommandParams, node: Node) -> void:
|
|
55
|
+
var node_name: String = reader.optional_string("name")
|
|
56
|
+
if not node_name.is_empty():
|
|
57
|
+
node.name = node_name
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Validates that every element of an array parameter is an object; the caller
|
|
61
|
+
# still routes through params_invalid().
|
|
62
|
+
func _require_dictionary_items(reader: CommandParams, param_name: String, items: Array) -> void:
|
|
63
|
+
if reader.failed():
|
|
64
|
+
return
|
|
65
|
+
for item: Variant in items:
|
|
66
|
+
if not item is Dictionary:
|
|
67
|
+
reader.fail("%s must contain objects" % param_name, {"param": param_name, "reason": "invalid_type"})
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
func _points_from(reader: CommandParams, param_name: String) -> PackedVector2Array:
|
|
72
|
+
var points: Array = reader.optional_array(param_name)
|
|
73
|
+
_require_dictionary_items(reader, param_name, points)
|
|
74
|
+
var packed: PackedVector2Array = PackedVector2Array()
|
|
75
|
+
if reader.failed():
|
|
76
|
+
return packed
|
|
77
|
+
for point: Dictionary in points:
|
|
78
|
+
@warning_ignore("return_value_discarded")
|
|
79
|
+
packed.append(_vector2_from(point))
|
|
80
|
+
return packed
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# --- TileMapLayer cells ---
|
|
84
|
+
func _cmd_tilemap(params: Dictionary) -> void:
|
|
85
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
86
|
+
var node: Node = require_node(reader)
|
|
87
|
+
var action: String = reader.optional_enum("action", "get_cell", ["set_cells", "get_cell", "erase_cells", "get_used_cells"])
|
|
88
|
+
if node != null and not node is TileMapLayer:
|
|
89
|
+
_require_class(reader, node, "TileMapLayer")
|
|
90
|
+
if params_invalid(reader):
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
var tilemap: TileMapLayer = node as TileMapLayer
|
|
94
|
+
match action:
|
|
95
|
+
"set_cells":
|
|
96
|
+
var cells: Array = reader.required_array("cells")
|
|
97
|
+
_require_dictionary_items(reader, "cells", cells)
|
|
98
|
+
if params_invalid(reader):
|
|
99
|
+
return
|
|
100
|
+
for cell: Dictionary in cells:
|
|
101
|
+
var pos: Vector2i = Vector2i(CommandParams.json_int(cell, "x"), CommandParams.json_int(cell, "y"))
|
|
102
|
+
var source_id: int = CommandParams.json_int(cell, "source_id")
|
|
103
|
+
var atlas_coords: Vector2i = Vector2i(CommandParams.json_int(cell, "atlas_x"), CommandParams.json_int(cell, "atlas_y"))
|
|
104
|
+
var alt_tile: int = CommandParams.json_int(cell, "alt_tile")
|
|
105
|
+
tilemap.set_cell(pos, source_id, atlas_coords, alt_tile)
|
|
106
|
+
respond({"success": true, "action": "set_cells", "count": cells.size()})
|
|
107
|
+
"get_cell":
|
|
108
|
+
var x: int = reader.optional_int("x", 0)
|
|
109
|
+
var y: int = reader.optional_int("y", 0)
|
|
110
|
+
if params_invalid(reader):
|
|
111
|
+
return
|
|
112
|
+
var pos: Vector2i = Vector2i(x, y)
|
|
113
|
+
respond({
|
|
114
|
+
"success": true, "action": "get_cell",
|
|
115
|
+
"x": x, "y": y,
|
|
116
|
+
"source_id": tilemap.get_cell_source_id(pos),
|
|
117
|
+
"atlas_coords": variant_to_json(tilemap.get_cell_atlas_coords(pos)),
|
|
118
|
+
"alt_tile": tilemap.get_cell_alternative_tile(pos)
|
|
119
|
+
})
|
|
120
|
+
"erase_cells":
|
|
121
|
+
var cells: Array = reader.required_array("cells")
|
|
122
|
+
_require_dictionary_items(reader, "cells", cells)
|
|
123
|
+
if params_invalid(reader):
|
|
124
|
+
return
|
|
125
|
+
for cell: Dictionary in cells:
|
|
126
|
+
tilemap.erase_cell(Vector2i(CommandParams.json_int(cell, "x"), CommandParams.json_int(cell, "y")))
|
|
127
|
+
respond({"success": true, "action": "erase_cells", "count": cells.size()})
|
|
128
|
+
"get_used_cells":
|
|
129
|
+
var source_filter: int = reader.optional_int("source_id", -1)
|
|
130
|
+
if params_invalid(reader):
|
|
131
|
+
return
|
|
132
|
+
var used: Array
|
|
133
|
+
if source_filter >= 0:
|
|
134
|
+
used = tilemap.get_used_cells_by_id(source_filter)
|
|
135
|
+
else:
|
|
136
|
+
used = tilemap.get_used_cells()
|
|
137
|
+
respond({"success": true, "action": "get_used_cells", "cells": variant_to_json(used), "count": used.size()})
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --- CanvasLayer / CanvasModulate ---
|
|
141
|
+
func _cmd_canvas(params: Dictionary) -> void:
|
|
142
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
143
|
+
var action: String = reader.optional_enum("action", "create_layer", ["create_layer", "create_modulate", "configure"])
|
|
144
|
+
if params_invalid(reader):
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
match action:
|
|
148
|
+
"create_layer":
|
|
149
|
+
var parent: Node = require_node(reader, "parent_path", "/root")
|
|
150
|
+
var layer: int = reader.optional_int("layer", 1)
|
|
151
|
+
if params_invalid(reader):
|
|
152
|
+
return
|
|
153
|
+
var cl: CanvasLayer = CanvasLayer.new()
|
|
154
|
+
if reader.has_param("layer"):
|
|
155
|
+
cl.layer = layer
|
|
156
|
+
_apply_optional_name(reader, cl)
|
|
157
|
+
parent.add_child(cl)
|
|
158
|
+
respond({"success": true, "action": "create_layer", "path": str(cl.get_path())})
|
|
159
|
+
"create_modulate":
|
|
160
|
+
var parent: Node = require_node(reader, "parent_path", "/root")
|
|
161
|
+
var color: Dictionary = reader.optional_dictionary("color")
|
|
162
|
+
if params_invalid(reader):
|
|
163
|
+
return
|
|
164
|
+
var cm: CanvasModulate = CanvasModulate.new()
|
|
165
|
+
if reader.has_param("color"):
|
|
166
|
+
cm.color = _color_from(color)
|
|
167
|
+
_apply_optional_name(reader, cm)
|
|
168
|
+
parent.add_child(cm)
|
|
169
|
+
respond({"success": true, "action": "create_modulate", "path": str(cm.get_path())})
|
|
170
|
+
"configure":
|
|
171
|
+
var node: Node = require_node(reader)
|
|
172
|
+
if node != null and not (node is CanvasLayer or node is CanvasModulate):
|
|
173
|
+
_require_class(reader, node, "CanvasLayer or CanvasModulate")
|
|
174
|
+
if params_invalid(reader):
|
|
175
|
+
return
|
|
176
|
+
var applied: Array = []
|
|
177
|
+
if node is CanvasLayer:
|
|
178
|
+
var cl2: CanvasLayer = node as CanvasLayer
|
|
179
|
+
var layer: int = reader.optional_int("layer", cl2.layer)
|
|
180
|
+
var offset: Dictionary = reader.optional_dictionary("offset")
|
|
181
|
+
var layer_visible: bool = reader.optional_bool("visible", cl2.visible)
|
|
182
|
+
if params_invalid(reader):
|
|
183
|
+
return
|
|
184
|
+
if reader.has_param("layer"):
|
|
185
|
+
cl2.layer = layer
|
|
186
|
+
applied.append("layer")
|
|
187
|
+
if reader.has_param("offset"):
|
|
188
|
+
cl2.offset = _vector2_from(offset)
|
|
189
|
+
applied.append("offset")
|
|
190
|
+
if reader.has_param("visible"):
|
|
191
|
+
cl2.visible = layer_visible
|
|
192
|
+
applied.append("visible")
|
|
193
|
+
elif node is CanvasModulate:
|
|
194
|
+
var color: Dictionary = reader.optional_dictionary("color")
|
|
195
|
+
if params_invalid(reader):
|
|
196
|
+
return
|
|
197
|
+
if reader.has_param("color"):
|
|
198
|
+
(node as CanvasModulate).color = _color_from(color)
|
|
199
|
+
applied.append("color")
|
|
200
|
+
respond({"success": true, "action": "configure", "applied": applied})
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# --- Immediate-mode canvas drawing ---
|
|
204
|
+
func _cmd_canvas_draw(params: Dictionary) -> void:
|
|
205
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
206
|
+
var action: String = reader.optional_enum("action", "line", ["line", "rect", "circle", "polygon", "text", "clear"])
|
|
207
|
+
if params_invalid(reader):
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
if action == "clear":
|
|
211
|
+
_draw_commands.clear()
|
|
212
|
+
if _canvas_draw_node != null and is_instance_valid(_canvas_draw_node):
|
|
213
|
+
_canvas_draw_node.queue_redraw()
|
|
214
|
+
respond({"success": true, "action": "clear"})
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
var color: Dictionary = reader.optional_dictionary("color", {"r": 1.0, "g": 1.0, "b": 1.0, "a": 1.0})
|
|
218
|
+
match action:
|
|
219
|
+
"line":
|
|
220
|
+
var _from: Dictionary = reader.required_dictionary("from")
|
|
221
|
+
var _to: Dictionary = reader.required_dictionary("to")
|
|
222
|
+
"rect":
|
|
223
|
+
var _rect: Dictionary = reader.required_dictionary("rect")
|
|
224
|
+
"circle":
|
|
225
|
+
var _center: Dictionary = reader.required_dictionary("center")
|
|
226
|
+
var radius: float = reader.required_number("radius", 0.0)
|
|
227
|
+
if not reader.failed() and radius <= 0.0:
|
|
228
|
+
reader.fail("radius must be greater than zero", {"param": "radius", "reason": "out_of_range", "min_exclusive": 0})
|
|
229
|
+
"polygon":
|
|
230
|
+
var points: Array = reader.required_array("points")
|
|
231
|
+
_require_dictionary_items(reader, "points", points)
|
|
232
|
+
if not reader.failed() and points.size() < 3:
|
|
233
|
+
reader.fail("points must contain at least 3 points", {"param": "points", "reason": "out_of_range", "min_items": 3})
|
|
234
|
+
"text":
|
|
235
|
+
var _position: Dictionary = reader.required_dictionary("position")
|
|
236
|
+
var _text: String = reader.required_string("text")
|
|
237
|
+
if params_invalid(reader):
|
|
238
|
+
return
|
|
239
|
+
if _canvas_draw_node == null or not is_instance_valid(_canvas_draw_node):
|
|
240
|
+
var parent: Node = require_node(reader, "parent_path", "/root")
|
|
241
|
+
if params_invalid(reader):
|
|
242
|
+
return
|
|
243
|
+
_canvas_draw_node = Node2D.new()
|
|
244
|
+
_canvas_draw_node.name = "_McpCanvasDraw"
|
|
245
|
+
_canvas_draw_node.set_script(_create_draw_script())
|
|
246
|
+
parent.add_child(_canvas_draw_node)
|
|
247
|
+
_canvas_draw_node.set("draw_commands", _draw_commands)
|
|
248
|
+
_draw_commands.append({"action": action, "params": params, "color": _color_from(color)})
|
|
249
|
+
_canvas_draw_node.set("draw_commands", _draw_commands)
|
|
250
|
+
_canvas_draw_node.queue_redraw()
|
|
251
|
+
respond({"success": true, "action": action})
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
func _create_draw_script() -> GDScript:
|
|
255
|
+
var s: GDScript = GDScript.new()
|
|
256
|
+
s.source_code = """extends Node2D
|
|
257
|
+
var draw_commands: Array = []
|
|
258
|
+
func _draw():
|
|
259
|
+
for cmd in draw_commands:
|
|
260
|
+
var p = cmd.params
|
|
261
|
+
var c = cmd.color
|
|
262
|
+
match cmd.action:
|
|
263
|
+
"line":
|
|
264
|
+
var f = p.get("from", {})
|
|
265
|
+
var t = p.get("to", {})
|
|
266
|
+
draw_line(Vector2(float(f.get("x",0)),float(f.get("y",0))),Vector2(float(t.get("x",0)),float(t.get("y",0))),c,float(p.get("width",2)))
|
|
267
|
+
"rect":
|
|
268
|
+
var r = p.get("rect", {})
|
|
269
|
+
draw_rect(Rect2(float(r.get("x",0)),float(r.get("y",0)),float(r.get("w",10)),float(r.get("h",10))),c,bool(p.get("filled",true)))
|
|
270
|
+
"circle":
|
|
271
|
+
var ct = p.get("center", {})
|
|
272
|
+
draw_circle(Vector2(float(ct.get("x",0)),float(ct.get("y",0))),float(p.get("radius",10)),c)
|
|
273
|
+
"polygon":
|
|
274
|
+
var pts = p.get("points", [])
|
|
275
|
+
var pv = PackedVector2Array()
|
|
276
|
+
for pt in pts:
|
|
277
|
+
pv.append(Vector2(float(pt.get("x",0)),float(pt.get("y",0))))
|
|
278
|
+
if pv.size() >= 3:
|
|
279
|
+
draw_colored_polygon(pv, c)
|
|
280
|
+
"text":
|
|
281
|
+
var pos = p.get("position", p.get("pos", {}))
|
|
282
|
+
draw_string(ThemeDB.fallback_font, Vector2(float(pos.get("x",0)),float(pos.get("y",0))), str(p.get("text","")), HORIZONTAL_ALIGNMENT_LEFT, -1, int(p.get("font_size",16)), c)
|
|
283
|
+
"""
|
|
284
|
+
@warning_ignore("return_value_discarded")
|
|
285
|
+
s.reload()
|
|
286
|
+
return s
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# --- 2D lights and occluders ---
|
|
290
|
+
func _cmd_light_2d(params: Dictionary) -> void:
|
|
291
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
292
|
+
var action: String = reader.optional_enum("action", "create_point", ["create_point", "create_directional", "create_occluder"])
|
|
293
|
+
var parent: Node = require_node(reader, "parent_path", "/root")
|
|
294
|
+
if params_invalid(reader):
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
match action:
|
|
298
|
+
"create_point":
|
|
299
|
+
var color: Dictionary = reader.optional_dictionary("color")
|
|
300
|
+
var energy: float = reader.optional_number("energy", 1.0)
|
|
301
|
+
var light_range: float = reader.optional_number("range", 1.0)
|
|
302
|
+
if params_invalid(reader):
|
|
303
|
+
return
|
|
304
|
+
var light: PointLight2D = PointLight2D.new()
|
|
305
|
+
if reader.has_param("color"):
|
|
306
|
+
light.color = _color_from(color)
|
|
307
|
+
if reader.has_param("energy"):
|
|
308
|
+
light.energy = energy
|
|
309
|
+
# Create a simple gradient texture for the light
|
|
310
|
+
var tex: GradientTexture2D = GradientTexture2D.new()
|
|
311
|
+
tex.width = 128
|
|
312
|
+
tex.height = 128
|
|
313
|
+
tex.fill = GradientTexture2D.FILL_RADIAL
|
|
314
|
+
tex.gradient = Gradient.new()
|
|
315
|
+
light.texture = tex
|
|
316
|
+
if reader.has_param("range"):
|
|
317
|
+
light.texture_scale = light_range
|
|
318
|
+
_apply_optional_name(reader, light)
|
|
319
|
+
parent.add_child(light)
|
|
320
|
+
respond({"success": true, "action": "create_point", "path": str(light.get_path())})
|
|
321
|
+
"create_directional":
|
|
322
|
+
var color: Dictionary = reader.optional_dictionary("color")
|
|
323
|
+
var energy: float = reader.optional_number("energy", 1.0)
|
|
324
|
+
if params_invalid(reader):
|
|
325
|
+
return
|
|
326
|
+
var light: DirectionalLight2D = DirectionalLight2D.new()
|
|
327
|
+
if reader.has_param("color"):
|
|
328
|
+
light.color = _color_from(color)
|
|
329
|
+
if reader.has_param("energy"):
|
|
330
|
+
light.energy = energy
|
|
331
|
+
_apply_optional_name(reader, light)
|
|
332
|
+
parent.add_child(light)
|
|
333
|
+
respond({"success": true, "action": "create_directional", "path": str(light.get_path())})
|
|
334
|
+
"create_occluder":
|
|
335
|
+
var points: Array = reader.required_array("points")
|
|
336
|
+
_require_dictionary_items(reader, "points", points)
|
|
337
|
+
if not reader.failed() and points.size() < 3:
|
|
338
|
+
reader.fail("points must contain at least 3 points", {"param": "points", "reason": "out_of_range", "min_items": 3})
|
|
339
|
+
var packed: PackedVector2Array = PackedVector2Array()
|
|
340
|
+
if not reader.failed():
|
|
341
|
+
for point: Dictionary in points:
|
|
342
|
+
@warning_ignore("return_value_discarded")
|
|
343
|
+
packed.append(_vector2_from(point))
|
|
344
|
+
if params_invalid(reader):
|
|
345
|
+
return
|
|
346
|
+
var occ: LightOccluder2D = LightOccluder2D.new()
|
|
347
|
+
var poly: OccluderPolygon2D = OccluderPolygon2D.new()
|
|
348
|
+
poly.polygon = packed
|
|
349
|
+
occ.occluder = poly
|
|
350
|
+
_apply_optional_name(reader, occ)
|
|
351
|
+
parent.add_child(occ)
|
|
352
|
+
respond({"success": true, "action": "create_occluder", "path": str(occ.get_path()), "point_count": packed.size()})
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# --- Parallax backgrounds and layers ---
|
|
356
|
+
func _cmd_parallax(params: Dictionary) -> void:
|
|
357
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
358
|
+
var action: String = reader.optional_enum("action", "create_background", ["create_background", "add_layer", "configure"])
|
|
359
|
+
if params_invalid(reader):
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
match action:
|
|
363
|
+
"create_background":
|
|
364
|
+
var parent: Node = require_node(reader, "parent_path", "/root")
|
|
365
|
+
if params_invalid(reader):
|
|
366
|
+
return
|
|
367
|
+
var bg: ParallaxBackground = ParallaxBackground.new()
|
|
368
|
+
_apply_optional_name(reader, bg)
|
|
369
|
+
parent.add_child(bg)
|
|
370
|
+
respond({"success": true, "action": "create_background", "path": str(bg.get_path())})
|
|
371
|
+
"add_layer":
|
|
372
|
+
var parent: Node = require_node(reader, "parent_path")
|
|
373
|
+
if parent != null and not parent is ParallaxBackground:
|
|
374
|
+
reader.fail("Node is not a ParallaxBackground: %s" % parent.get_class(),
|
|
375
|
+
{"param": "parent_path", "reason": "invalid_value", "expected": "ParallaxBackground", "value": parent.get_class()})
|
|
376
|
+
var motion_scale: Dictionary = reader.optional_dictionary("motion_scale")
|
|
377
|
+
var motion_offset: Dictionary = reader.optional_dictionary("motion_offset")
|
|
378
|
+
var mirroring: Dictionary = reader.optional_dictionary("mirroring")
|
|
379
|
+
if params_invalid(reader):
|
|
380
|
+
return
|
|
381
|
+
var layer: ParallaxLayer = ParallaxLayer.new()
|
|
382
|
+
if reader.has_param("motion_scale"):
|
|
383
|
+
layer.motion_scale = _vector2_from(motion_scale, 1, 1)
|
|
384
|
+
if reader.has_param("motion_offset"):
|
|
385
|
+
layer.motion_offset = _vector2_from(motion_offset)
|
|
386
|
+
if reader.has_param("mirroring"):
|
|
387
|
+
layer.motion_mirroring = _vector2_from(mirroring)
|
|
388
|
+
_apply_optional_name(reader, layer)
|
|
389
|
+
parent.add_child(layer)
|
|
390
|
+
respond({"success": true, "action": "add_layer", "path": str(layer.get_path())})
|
|
391
|
+
"configure":
|
|
392
|
+
var node: Node = require_node(reader)
|
|
393
|
+
if node != null and not (node is ParallaxBackground or node is ParallaxLayer):
|
|
394
|
+
_require_class(reader, node, "ParallaxBackground or ParallaxLayer")
|
|
395
|
+
if params_invalid(reader):
|
|
396
|
+
return
|
|
397
|
+
var applied: Array = []
|
|
398
|
+
if node is ParallaxBackground:
|
|
399
|
+
var pbg: ParallaxBackground = node as ParallaxBackground
|
|
400
|
+
var scroll_offset: Dictionary = reader.optional_dictionary("scroll_offset")
|
|
401
|
+
var scroll_base_offset: Dictionary = reader.optional_dictionary("scroll_base_offset")
|
|
402
|
+
if params_invalid(reader):
|
|
403
|
+
return
|
|
404
|
+
if reader.has_param("scroll_offset"):
|
|
405
|
+
pbg.scroll_offset = _vector2_from(scroll_offset)
|
|
406
|
+
applied.append("scroll_offset")
|
|
407
|
+
if reader.has_param("scroll_base_offset"):
|
|
408
|
+
pbg.scroll_base_offset = _vector2_from(scroll_base_offset)
|
|
409
|
+
applied.append("scroll_base_offset")
|
|
410
|
+
elif node is ParallaxLayer:
|
|
411
|
+
var pl: ParallaxLayer = node as ParallaxLayer
|
|
412
|
+
var motion_scale: Dictionary = reader.optional_dictionary("motion_scale")
|
|
413
|
+
var motion_offset: Dictionary = reader.optional_dictionary("motion_offset")
|
|
414
|
+
var mirroring: Dictionary = reader.optional_dictionary("mirroring")
|
|
415
|
+
if params_invalid(reader):
|
|
416
|
+
return
|
|
417
|
+
if reader.has_param("motion_scale"):
|
|
418
|
+
pl.motion_scale = _vector2_from(motion_scale, 1, 1)
|
|
419
|
+
applied.append("motion_scale")
|
|
420
|
+
if reader.has_param("motion_offset"):
|
|
421
|
+
pl.motion_offset = _vector2_from(motion_offset)
|
|
422
|
+
applied.append("motion_offset")
|
|
423
|
+
if reader.has_param("mirroring"):
|
|
424
|
+
pl.motion_mirroring = _vector2_from(mirroring)
|
|
425
|
+
applied.append("mirroring")
|
|
426
|
+
respond({"success": true, "action": "configure", "applied": applied})
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# --- Line2D / Polygon2D points ---
|
|
430
|
+
func _cmd_shape_2d(params: Dictionary) -> void:
|
|
431
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
432
|
+
var node: Node = require_node(reader)
|
|
433
|
+
var action: String = reader.optional_enum("action", "get_points", ["add_point", "set_points", "clear", "get_points"])
|
|
434
|
+
if node != null and not (node is Line2D or node is Polygon2D):
|
|
435
|
+
_require_class(reader, node, "Line2D or Polygon2D")
|
|
436
|
+
if params_invalid(reader):
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
match action:
|
|
440
|
+
"add_point":
|
|
441
|
+
var point: Dictionary = reader.required_dictionary("point")
|
|
442
|
+
if params_invalid(reader):
|
|
443
|
+
return
|
|
444
|
+
var pt: Vector2 = _vector2_from(point)
|
|
445
|
+
if node is Line2D:
|
|
446
|
+
(node as Line2D).add_point(pt)
|
|
447
|
+
else:
|
|
448
|
+
var polygon: PackedVector2Array = (node as Polygon2D).polygon
|
|
449
|
+
@warning_ignore("return_value_discarded")
|
|
450
|
+
polygon.append(pt)
|
|
451
|
+
(node as Polygon2D).polygon = polygon
|
|
452
|
+
respond({"success": true, "action": "add_point"})
|
|
453
|
+
"set_points":
|
|
454
|
+
var points: Array = reader.required_array("points")
|
|
455
|
+
_require_dictionary_items(reader, "points", points)
|
|
456
|
+
var packed: PackedVector2Array = PackedVector2Array()
|
|
457
|
+
if not reader.failed():
|
|
458
|
+
for point: Dictionary in points:
|
|
459
|
+
@warning_ignore("return_value_discarded")
|
|
460
|
+
packed.append(_vector2_from(point))
|
|
461
|
+
if params_invalid(reader):
|
|
462
|
+
return
|
|
463
|
+
if node is Line2D:
|
|
464
|
+
(node as Line2D).points = packed
|
|
465
|
+
else:
|
|
466
|
+
(node as Polygon2D).polygon = packed
|
|
467
|
+
respond({"success": true, "action": "set_points", "count": packed.size()})
|
|
468
|
+
"clear":
|
|
469
|
+
if node is Line2D:
|
|
470
|
+
(node as Line2D).clear_points()
|
|
471
|
+
else:
|
|
472
|
+
(node as Polygon2D).polygon = PackedVector2Array()
|
|
473
|
+
respond({"success": true, "action": "clear"})
|
|
474
|
+
"get_points":
|
|
475
|
+
var pts: PackedVector2Array
|
|
476
|
+
if node is Line2D:
|
|
477
|
+
pts = (node as Line2D).points
|
|
478
|
+
else:
|
|
479
|
+
pts = (node as Polygon2D).polygon
|
|
480
|
+
var result: Array = []
|
|
481
|
+
for p: Vector2 in pts:
|
|
482
|
+
result.append({"x": p.x, "y": p.y})
|
|
483
|
+
respond({"success": true, "action": "get_points", "points": result})
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# --- Path2D curves ---
|
|
487
|
+
func _cmd_path_2d(params: Dictionary) -> void:
|
|
488
|
+
var reader: CommandParams = CommandParams.new(params)
|
|
489
|
+
var action: String = reader.optional_enum("action", "create", ["create", "add_point", "get_points"])
|
|
490
|
+
if params_invalid(reader):
|
|
491
|
+
return
|
|
492
|
+
|
|
493
|
+
match action:
|
|
494
|
+
"create":
|
|
495
|
+
var parent: Node = require_node(reader, "parent_path", "/root")
|
|
496
|
+
var packed: PackedVector2Array = _points_from(reader, "points")
|
|
497
|
+
if params_invalid(reader):
|
|
498
|
+
return
|
|
499
|
+
var path_node: Path2D = Path2D.new()
|
|
500
|
+
path_node.curve = Curve2D.new()
|
|
501
|
+
_apply_optional_name(reader, path_node)
|
|
502
|
+
for pt: Vector2 in packed:
|
|
503
|
+
path_node.curve.add_point(pt)
|
|
504
|
+
parent.add_child(path_node)
|
|
505
|
+
respond({"success": true, "action": "create", "path": str(path_node.get_path()), "point_count": path_node.curve.point_count})
|
|
506
|
+
"add_point":
|
|
507
|
+
var node: Node = require_node(reader)
|
|
508
|
+
if node != null and not node is Path2D:
|
|
509
|
+
_require_class(reader, node, "Path2D")
|
|
510
|
+
var point: Dictionary = reader.required_dictionary("point")
|
|
511
|
+
if params_invalid(reader):
|
|
512
|
+
return
|
|
513
|
+
var path_node: Path2D = node as Path2D
|
|
514
|
+
path_node.curve.add_point(_vector2_from(point))
|
|
515
|
+
respond({"success": true, "action": "add_point", "point_count": path_node.curve.point_count})
|
|
516
|
+
"get_points":
|
|
517
|
+
var node: Node = require_node(reader)
|
|
518
|
+
if node != null and not node is Path2D:
|
|
519
|
+
_require_class(reader, node, "Path2D")
|
|
520
|
+
if params_invalid(reader):
|
|
521
|
+
return
|
|
522
|
+
var path_node: Path2D = node as Path2D
|
|
523
|
+
var pts: Array = []
|
|
524
|
+
for i: int in path_node.curve.point_count:
|
|
525
|
+
var pt: Vector2 = path_node.curve.get_point_position(i)
|
|
526
|
+
pts.append({"x": pt.x, "y": pt.y})
|
|
527
|
+
respond({"success": true, "action": "get_points", "points": pts})
|