@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.
Files changed (70) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +925 -0
  3. package/addons/godot_agent_loop/LICENSE +23 -0
  4. package/addons/godot_agent_loop/README.md +31 -0
  5. package/addons/godot_agent_loop/plugin.cfg +8 -0
  6. package/addons/godot_agent_loop/plugin.gd +417 -0
  7. package/agent-plugin/.claude-plugin/plugin.json +19 -0
  8. package/agent-plugin/.codex-plugin/plugin.json +37 -0
  9. package/agent-plugin/.mcp.json +14 -0
  10. package/agent-plugin/adapter-manifest.json +45 -0
  11. package/agent-plugin/pi/extension.ts +136 -0
  12. package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
  13. package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
  14. package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
  15. package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
  16. package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
  17. package/build/authoring-session-manager.js +237 -0
  18. package/build/domain-tool-registries.js +207 -0
  19. package/build/editor-bridge-protocol.js +1 -0
  20. package/build/editor-connection.js +112 -0
  21. package/build/editor-mutation-guard.js +30 -0
  22. package/build/editor-plugin-installer.js +220 -0
  23. package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
  24. package/build/game-command-service.js +49 -0
  25. package/build/game-connection.js +337 -0
  26. package/build/godot-executable.js +156 -0
  27. package/build/godot-process-manager.js +112 -0
  28. package/build/godot-subprocess.js +23 -0
  29. package/build/headless-operation-runner.js +68 -0
  30. package/build/headless-operation-service.js +65 -0
  31. package/build/index.js +478 -0
  32. package/build/interaction-server-installer.js +234 -0
  33. package/build/opencode-setup.js +199 -0
  34. package/build/project-support.js +219 -0
  35. package/build/runtime-protocol.js +202 -0
  36. package/build/scripts/godot_operations.gd +1534 -0
  37. package/build/scripts/mcp_editor_plugin.gd +417 -0
  38. package/build/scripts/mcp_interaction_server.gd +1255 -0
  39. package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
  40. package/build/scripts/mcp_runtime/command_params.gd +339 -0
  41. package/build/scripts/mcp_runtime/core_domain.gd +591 -0
  42. package/build/scripts/mcp_runtime/input_domain.gd +695 -0
  43. package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
  44. package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
  45. package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
  46. package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
  47. package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
  48. package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
  49. package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
  50. package/build/scripts/mcp_runtime/system_domain.gd +277 -0
  51. package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
  52. package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
  53. package/build/scripts/validate_script.gd +28 -0
  54. package/build/server-instructions.js +11 -0
  55. package/build/session-timing.js +20 -0
  56. package/build/tool-argument-validation.js +120 -0
  57. package/build/tool-definitions.js +2727 -0
  58. package/build/tool-handlers/game-tool-handlers.js +1345 -0
  59. package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
  60. package/build/tool-handlers/project-handler-services.js +1458 -0
  61. package/build/tool-handlers/project-tool-handlers.js +1506 -0
  62. package/build/tool-handlers/visual-regression-service.js +139 -0
  63. package/build/tool-manifest.js +1193 -0
  64. package/build/tool-mutation-policy.js +122 -0
  65. package/build/tool-registry.js +34 -0
  66. package/build/tool-surface.js +70 -0
  67. package/build/utils.js +273 -0
  68. package/package.json +110 -0
  69. package/product.json +52 -0
  70. package/scripts/prepare-package.js +42 -0
@@ -0,0 +1,807 @@
1
+ extends "res://mcp_runtime/runtime_domain.gd"
2
+
3
+ # Rendering and environment domain. Stateful visual-shader graphs and debug-draw
4
+ # objects live here with the handlers that own them.
5
+
6
+ func register_commands() -> void:
7
+ register_command("get_camera", _cmd_get_camera)
8
+ register_command("set_camera", _cmd_set_camera)
9
+ register_command("camera_attributes", _cmd_camera_attributes)
10
+ register_command("set_shader_param", _cmd_set_shader_param)
11
+ register_command("visual_shader", _cmd_visual_shader)
12
+ register_command("environment", _cmd_environment)
13
+ register_command("set_particles", _cmd_set_particles)
14
+ register_command("viewport", _cmd_viewport)
15
+ register_command("debug_draw", _cmd_debug_draw)
16
+ register_command("render_settings", _cmd_render_settings)
17
+ register_command("sky", _cmd_sky)
18
+ register_command("gi", _cmd_gi)
19
+ register_command("video", _cmd_video)
20
+
21
+
22
+ func _exit_tree() -> void:
23
+ _clear_debug_draw()
24
+ _visual_shaders.clear()
25
+
26
+
27
+ func _process(_delta: float) -> void:
28
+ for index in range(_debug_meshes.size() - 1, -1, -1):
29
+ var entry: Dictionary = _debug_meshes[index]
30
+ var frames_left: int = entry.get("frames_left", 0)
31
+ if frames_left <= 0:
32
+ continue
33
+ frames_left -= 1
34
+ if frames_left == 0:
35
+ var drawn: Variant = entry.get("node")
36
+ if drawn is Node and is_instance_valid(drawn):
37
+ var node: Node = drawn
38
+ node.queue_free()
39
+ _debug_meshes.remove_at(index)
40
+ else:
41
+ entry["frames_left"] = frames_left
42
+
43
+
44
+ func _as_environment(value: Variant) -> Environment:
45
+ if value is Environment:
46
+ return value
47
+ return null
48
+
49
+
50
+ func _clear_debug_draw() -> void:
51
+ for entry: Dictionary in _debug_meshes:
52
+ var drawn: Variant = entry.get("node")
53
+ if drawn is Node and is_instance_valid(drawn):
54
+ var node: Node = drawn
55
+ node.queue_free()
56
+ _debug_meshes.clear()
57
+ if _debug_draw_node != null and is_instance_valid(_debug_draw_node):
58
+ _debug_draw_node.queue_free()
59
+ _debug_draw_node = null
60
+
61
+ func _cmd_get_camera(_params: Dictionary) -> void:
62
+ var result: Dictionary = {"success": true}
63
+
64
+ var cam2d: Camera2D = get_viewport().get_camera_2d()
65
+ if cam2d != null:
66
+ result["camera_2d"] = {
67
+ "position": {"x": cam2d.global_position.x, "y": cam2d.global_position.y},
68
+ "rotation": cam2d.global_rotation,
69
+ "zoom": {"x": cam2d.zoom.x, "y": cam2d.zoom.y},
70
+ "path": str(cam2d.get_path())
71
+ }
72
+
73
+ var cam3d: Camera3D = get_viewport().get_camera_3d()
74
+ if cam3d != null:
75
+ result["camera_3d"] = {
76
+ "position": {"x": cam3d.global_position.x, "y": cam3d.global_position.y, "z": cam3d.global_position.z},
77
+ "rotation": {"x": rad_to_deg(cam3d.global_rotation.x), "y": rad_to_deg(cam3d.global_rotation.y), "z": rad_to_deg(cam3d.global_rotation.z)},
78
+ "fov": cam3d.fov,
79
+ "path": str(cam3d.get_path())
80
+ }
81
+
82
+ if cam2d == null and cam3d == null:
83
+ result["error"] = "No active camera found"
84
+ result["success"] = false
85
+
86
+ respond(result)
87
+
88
+
89
+ # --- Set Camera ---
90
+ func _cmd_set_camera(params: Dictionary) -> void:
91
+ var cam2d: Camera2D = get_viewport().get_camera_2d()
92
+ var cam3d: Camera3D = get_viewport().get_camera_3d()
93
+
94
+ if cam2d == null and cam3d == null:
95
+ respond({"error": "No active camera found"})
96
+ return
97
+
98
+ if cam2d != null:
99
+ if params.has("position"):
100
+ var pos: Dictionary = params["position"]
101
+ cam2d.global_position = Vector2(CommandParams.json_float(pos, "x", cam2d.global_position.x), CommandParams.json_float(pos, "y", cam2d.global_position.y))
102
+ if params.has("rotation"):
103
+ var rot: Dictionary = params["rotation"]
104
+ cam2d.global_rotation = deg_to_rad(CommandParams.json_float(rot, "z", rad_to_deg(cam2d.global_rotation)))
105
+ if params.has("zoom"):
106
+ var z: Dictionary = params["zoom"]
107
+ cam2d.zoom = Vector2(CommandParams.json_float(z, "x", cam2d.zoom.x), CommandParams.json_float(z, "y", cam2d.zoom.y))
108
+ respond({"success": true, "camera": "2d", "position": variant_to_json(cam2d.global_position), "zoom": variant_to_json(cam2d.zoom)})
109
+ return
110
+
111
+ if cam3d != null:
112
+ if params.has("position"):
113
+ var pos: Dictionary = params["position"]
114
+ cam3d.global_position = Vector3(CommandParams.json_float(pos, "x", cam3d.global_position.x), CommandParams.json_float(pos, "y", cam3d.global_position.y), CommandParams.json_float(pos, "z", cam3d.global_position.z))
115
+ if params.has("rotation"):
116
+ var rot: Dictionary = params["rotation"]
117
+ cam3d.global_rotation = Vector3(deg_to_rad(CommandParams.json_float(rot, "x", rad_to_deg(cam3d.global_rotation.x))), deg_to_rad(CommandParams.json_float(rot, "y", rad_to_deg(cam3d.global_rotation.y))), deg_to_rad(CommandParams.json_float(rot, "z", rad_to_deg(cam3d.global_rotation.z))))
118
+ if params.has("fov"):
119
+ cam3d.fov = CommandParams.to_float(params["fov"])
120
+ respond({"success": true, "camera": "3d", "position": variant_to_json(cam3d.global_position), "rotation": variant_to_json(cam3d.global_rotation)})
121
+ return
122
+
123
+
124
+ # --- Get Audio ---
125
+ func _cmd_set_shader_param(params: Dictionary) -> void:
126
+ var reader := CommandParams.new(params)
127
+ var node: Node = require_node(reader)
128
+ var param_name: String = reader.required_string("param_name")
129
+ if params_invalid(reader):
130
+ return
131
+ var node_path: String = str(node.get_path())
132
+
133
+ var material: Material = null
134
+ # Try material_override first (MeshInstance3D/2D)
135
+ if node.get("material_override") != null:
136
+ material = node.get("material_override")
137
+ # Try surface override material (MeshInstance3D)
138
+ elif node is MeshInstance3D:
139
+ var mesh_instance: MeshInstance3D = node
140
+ material = mesh_instance.get_surface_override_material(0)
141
+ # Try material property (CanvasItem, e.g. Sprite2D)
142
+ elif node.get("material") != null:
143
+ material = node.get("material")
144
+
145
+ if material == null or not material is ShaderMaterial:
146
+ respond({"error": "No ShaderMaterial found on node: %s" % node_path})
147
+ return
148
+
149
+ var shader_mat: ShaderMaterial = material as ShaderMaterial
150
+ var raw_value: Variant = params.get("value", null)
151
+ var type_hint: String = params.get("type_hint", "")
152
+ var value: Variant = json_to_variant(raw_value, type_hint)
153
+ shader_mat.set_shader_parameter(param_name, value)
154
+ respond({"success": true, "node_path": node_path, "param_name": param_name, "value": variant_to_json(shader_mat.get_shader_parameter(param_name))})
155
+
156
+
157
+ # --- Visual Shader ---
158
+ # Shaders built through the visual_shader command live here until applied to a
159
+ # node; ids let a client build several graphs. Edits target the fragment
160
+ # function, which is where the tool's node/connection workflow operates.
161
+ var _visual_shaders: Dictionary = {}
162
+ var _next_visual_shader_id: int = 1
163
+
164
+ const VISUAL_SHADER_MODES: Dictionary = {
165
+ "spatial": Shader.MODE_SPATIAL,
166
+ "canvas_item": Shader.MODE_CANVAS_ITEM,
167
+ "particles": Shader.MODE_PARTICLES,
168
+ "sky": Shader.MODE_SKY,
169
+ "fog": Shader.MODE_FOG,
170
+ }
171
+
172
+ func _cmd_visual_shader(params: Dictionary) -> void:
173
+ var reader := CommandParams.new(params)
174
+ var action: String = reader.required_enum("action", ["create", "add_node", "connect", "disconnect", "get_nodes", "apply"])
175
+ if params_invalid(reader):
176
+ return
177
+
178
+ if action == "create":
179
+ var shader_type: String = params.get("shader_type", "spatial")
180
+ if not VISUAL_SHADER_MODES.has(shader_type):
181
+ respond({"error": "Unknown shader_type: %s" % shader_type})
182
+ return
183
+ var created: VisualShader = VisualShader.new()
184
+ var mode: Shader.Mode = VISUAL_SHADER_MODES[shader_type]
185
+ created.set_mode(mode)
186
+ var shader_id: int = _next_visual_shader_id
187
+ _next_visual_shader_id += 1
188
+ _visual_shaders[shader_id] = created
189
+ respond({"success": true, "shader_id": shader_id, "shader_type": shader_type})
190
+ return
191
+
192
+ # Every other action edits an existing graph: the one named by shader_id,
193
+ # or the most recently created one.
194
+ var target_id: int = reader.optional_int("shader_id", _next_visual_shader_id - 1, 1)
195
+ if params_invalid(reader):
196
+ return
197
+ var shader: VisualShader = _visual_shaders.get(target_id)
198
+ if shader == null:
199
+ respond({"error": "No visual shader with id %s; use action create first" % target_id})
200
+ return
201
+
202
+ match action:
203
+ "add_node":
204
+ var node_class: String = reader.required_string("node_class")
205
+ if params_invalid(reader):
206
+ return
207
+ if not ClassDB.class_exists(node_class) or not ClassDB.is_parent_class(node_class, "VisualShaderNode"):
208
+ respond({"error": "Class '%s' is not a VisualShaderNode type" % node_class})
209
+ return
210
+ var instantiated: Variant = ClassDB.instantiate(node_class)
211
+ if not instantiated is VisualShaderNode:
212
+ respond({"error": "Failed to instantiate: %s" % node_class})
213
+ return
214
+ var graph_node: VisualShaderNode = instantiated
215
+ var position: Dictionary = CommandParams.json_dictionary(params, "position")
216
+ var node_id: int = shader.get_valid_node_id(VisualShader.TYPE_FRAGMENT)
217
+ shader.add_node(VisualShader.TYPE_FRAGMENT, graph_node, CommandParams.to_vector2(position), node_id)
218
+ respond({"success": true, "shader_id": target_id, "node_id": node_id, "node_class": node_class})
219
+ "connect":
220
+ var from_node: int = reader.required_int("from_node", 0)
221
+ var from_port: int = reader.required_int("from_port", 0)
222
+ var to_node: int = reader.required_int("to_node", 0)
223
+ var to_port: int = reader.required_int("to_port", 0)
224
+ if params_invalid(reader):
225
+ return
226
+ var err: int = shader.connect_nodes(VisualShader.TYPE_FRAGMENT, from_node, from_port, to_node, to_port)
227
+ if err != OK:
228
+ respond({"error": "Failed to connect nodes (error %d)" % err})
229
+ return
230
+ respond({"success": true, "shader_id": target_id})
231
+ "disconnect":
232
+ var from_node: int = reader.required_int("from_node", 0)
233
+ var from_port: int = reader.required_int("from_port", 0)
234
+ var to_node: int = reader.required_int("to_node", 0)
235
+ var to_port: int = reader.required_int("to_port", 0)
236
+ if params_invalid(reader):
237
+ return
238
+ if not shader.is_node_connection(VisualShader.TYPE_FRAGMENT, from_node, from_port, to_node, to_port):
239
+ respond({"error": "Visual shader connection does not exist"})
240
+ return
241
+ shader.disconnect_nodes(VisualShader.TYPE_FRAGMENT, from_node, from_port, to_node, to_port)
242
+ respond({"success": true, "shader_id": target_id})
243
+ "get_nodes":
244
+ var nodes: Array = []
245
+ for node_id in shader.get_node_list(VisualShader.TYPE_FRAGMENT):
246
+ var graph_node: VisualShaderNode = shader.get_node(VisualShader.TYPE_FRAGMENT, node_id)
247
+ var node_position: Vector2 = shader.get_node_position(VisualShader.TYPE_FRAGMENT, node_id)
248
+ nodes.append({"id": node_id, "class": graph_node.get_class(), "position": {"x": node_position.x, "y": node_position.y}})
249
+ respond({"success": true, "shader_id": target_id, "nodes": nodes})
250
+ "apply":
251
+ var node_path: String = reader.required_node_path("node_path")
252
+ if params_invalid(reader):
253
+ return
254
+ var node: Node = get_tree().root.get_node_or_null(node_path)
255
+ if node == null:
256
+ respond({"error": "Node not found: %s" % node_path})
257
+ return
258
+ var material: ShaderMaterial = ShaderMaterial.new()
259
+ material.shader = shader
260
+ if "material_override" in node:
261
+ node.set("material_override", material)
262
+ elif "material" in node:
263
+ node.set("material", material)
264
+ else:
265
+ respond({"error": "Node has no material property: %s" % node_path})
266
+ return
267
+ respond({"success": true, "shader_id": target_id, "node_path": node_path})
268
+ _:
269
+ respond({"error": "Unknown action: %s" % action})
270
+
271
+
272
+ # --- Audio Play ---
273
+ func _cmd_environment(params: Dictionary) -> void:
274
+ var reader := CommandParams.new(params)
275
+ var action: String = reader.optional_enum("action", "set", ["get", "set"])
276
+ if params_invalid(reader):
277
+ return
278
+
279
+ # Find existing WorldEnvironment or Camera3D environment
280
+ var env: Environment = null
281
+ var world_env: Node = null
282
+
283
+ # Search for WorldEnvironment node
284
+ var found: Array[Node] = get_tree().root.find_children("*", "WorldEnvironment", true, false)
285
+ if found.size() > 0:
286
+ world_env = found[0]
287
+ if world_env != null:
288
+ env = _as_environment(world_env.get("environment"))
289
+
290
+ # Fallback: check Camera3D
291
+ if env == null:
292
+ var cam3d: Camera3D = get_viewport().get_camera_3d()
293
+ if cam3d != null:
294
+ env = _as_environment(cam3d.get("environment"))
295
+
296
+ if action == "get":
297
+ if env == null:
298
+ respond({"error": "No Environment resource found"})
299
+ return
300
+ respond(_get_environment_state(env))
301
+ return
302
+
303
+ # action == "set": create if needed
304
+ if env == null:
305
+ env = Environment.new()
306
+ var we: WorldEnvironment = WorldEnvironment.new()
307
+ we.environment = env
308
+ get_tree().root.add_child(we)
309
+ world_env = we
310
+
311
+ # Apply settings
312
+ if params.has("background_mode"):
313
+ env.background_mode = CommandParams.to_int(params["background_mode"]) as Environment.BGMode
314
+ if params.has("background_color"):
315
+ var c: Dictionary = params["background_color"]
316
+ env.background_color = Color(CommandParams.json_float(c, "r", 0), CommandParams.json_float(c, "g", 0), CommandParams.json_float(c, "b", 0), CommandParams.json_float(c, "a", 1))
317
+ if params.has("ambient_light_color"):
318
+ var c: Dictionary = params["ambient_light_color"]
319
+ env.ambient_light_color = Color(CommandParams.json_float(c, "r", 0), CommandParams.json_float(c, "g", 0), CommandParams.json_float(c, "b", 0), CommandParams.json_float(c, "a", 1))
320
+ if params.has("ambient_light_energy"):
321
+ env.ambient_light_energy = CommandParams.to_float(params["ambient_light_energy"])
322
+ if params.has("fog_enabled"):
323
+ env.fog_enabled = CommandParams.to_bool(params["fog_enabled"])
324
+ if params.has("fog_density"):
325
+ env.fog_density = CommandParams.to_float(params["fog_density"])
326
+ if params.has("fog_light_color"):
327
+ var c: Dictionary = params["fog_light_color"]
328
+ env.fog_light_color = Color(CommandParams.json_float(c, "r", 0), CommandParams.json_float(c, "g", 0), CommandParams.json_float(c, "b", 0), CommandParams.json_float(c, "a", 1))
329
+ if params.has("glow_enabled"):
330
+ env.glow_enabled = CommandParams.to_bool(params["glow_enabled"])
331
+ if params.has("glow_intensity"):
332
+ env.glow_intensity = CommandParams.to_float(params["glow_intensity"])
333
+ if params.has("glow_bloom"):
334
+ env.glow_bloom = CommandParams.to_float(params["glow_bloom"])
335
+ if params.has("tonemap_mode"):
336
+ env.tonemap_mode = CommandParams.to_int(params["tonemap_mode"]) as Environment.ToneMapper
337
+ if params.has("ssao_enabled"):
338
+ env.ssao_enabled = CommandParams.to_bool(params["ssao_enabled"])
339
+ if params.has("ssao_radius"):
340
+ env.ssao_radius = CommandParams.to_float(params["ssao_radius"])
341
+ if params.has("ssao_intensity"):
342
+ env.ssao_intensity = CommandParams.to_float(params["ssao_intensity"])
343
+ if params.has("ssr_enabled"):
344
+ env.ssr_enabled = CommandParams.to_bool(params["ssr_enabled"])
345
+ if params.has("brightness"):
346
+ env.adjustment_enabled = true
347
+ env.adjustment_brightness = CommandParams.to_float(params["brightness"])
348
+ if params.has("contrast"):
349
+ env.adjustment_enabled = true
350
+ env.adjustment_contrast = CommandParams.to_float(params["contrast"])
351
+ if params.has("saturation"):
352
+ env.adjustment_enabled = true
353
+ env.adjustment_saturation = CommandParams.to_float(params["saturation"])
354
+
355
+ respond(_get_environment_state(env))
356
+
357
+
358
+ func _get_environment_state(env: Environment) -> Dictionary:
359
+ return {
360
+ "success": true,
361
+ "background_mode": env.background_mode,
362
+ "background_color": variant_to_json(env.background_color),
363
+ "ambient_light_color": variant_to_json(env.ambient_light_color),
364
+ "ambient_light_energy": env.ambient_light_energy,
365
+ "fog_enabled": env.fog_enabled,
366
+ "fog_density": env.fog_density,
367
+ "fog_light_color": variant_to_json(env.fog_light_color),
368
+ "glow_enabled": env.glow_enabled,
369
+ "glow_intensity": env.glow_intensity,
370
+ "glow_bloom": env.glow_bloom,
371
+ "tonemap_mode": env.tonemap_mode,
372
+ "ssao_enabled": env.ssao_enabled,
373
+ "ssao_radius": env.ssao_radius,
374
+ "ssao_intensity": env.ssao_intensity,
375
+ "ssr_enabled": env.ssr_enabled,
376
+ "brightness": env.adjustment_brightness,
377
+ "contrast": env.adjustment_contrast,
378
+ "saturation": env.adjustment_saturation
379
+ }
380
+
381
+
382
+ # --- Manage Group ---
383
+ func _cmd_set_particles(params: Dictionary) -> void:
384
+ var node_path: String = params.get("node_path", "")
385
+ if node_path.is_empty():
386
+ respond({"error": "node_path is required"})
387
+ return
388
+
389
+ var node: Node = get_tree().root.get_node_or_null(node_path)
390
+ if node == null:
391
+ respond({"error": "Node not found: %s" % node_path})
392
+ return
393
+
394
+ if not (node is GPUParticles2D or node is GPUParticles3D):
395
+ respond({"error": "Node is not a GPUParticles node: %s (is %s)" % [node_path, node.get_class()]})
396
+ return
397
+
398
+ # Set direct particle properties
399
+ if params.has("emitting"):
400
+ node.set("emitting", CommandParams.to_bool(params["emitting"]))
401
+ if params.has("amount"):
402
+ node.set("amount", CommandParams.to_int(params["amount"]))
403
+ if params.has("lifetime"):
404
+ node.set("lifetime", CommandParams.to_float(params["lifetime"]))
405
+ if params.has("one_shot"):
406
+ node.set("one_shot", CommandParams.to_bool(params["one_shot"]))
407
+ if params.has("speed_scale"):
408
+ node.set("speed_scale", CommandParams.to_float(params["speed_scale"]))
409
+ if params.has("explosiveness"):
410
+ node.set("explosiveness", CommandParams.to_float(params["explosiveness"]))
411
+ if params.has("randomness"):
412
+ node.set("randomness", CommandParams.to_float(params["randomness"]))
413
+
414
+ # Configure process material
415
+ if params.has("process_material"):
416
+ var mat_params: Dictionary = params["process_material"]
417
+ var process_material: Variant = node.get("process_material")
418
+ var mat: ParticleProcessMaterial = null
419
+ if process_material is ParticleProcessMaterial:
420
+ mat = process_material
421
+ if mat == null:
422
+ mat = ParticleProcessMaterial.new()
423
+ node.set("process_material", mat)
424
+ if mat_params.has("direction"):
425
+ var d: Dictionary = mat_params["direction"]
426
+ mat.direction = Vector3(CommandParams.json_float(d, "x", 0), CommandParams.json_float(d, "y", -1), CommandParams.json_float(d, "z", 0))
427
+ if mat_params.has("spread"):
428
+ mat.spread = CommandParams.to_float(mat_params["spread"])
429
+ if mat_params.has("gravity"):
430
+ var g: Dictionary = mat_params["gravity"]
431
+ mat.gravity = Vector3(CommandParams.json_float(g, "x", 0), CommandParams.json_float(g, "y", -9.8), CommandParams.json_float(g, "z", 0))
432
+ if mat_params.has("initial_velocity_min"):
433
+ mat.initial_velocity_min = CommandParams.to_float(mat_params["initial_velocity_min"])
434
+ if mat_params.has("initial_velocity_max"):
435
+ mat.initial_velocity_max = CommandParams.to_float(mat_params["initial_velocity_max"])
436
+ if mat_params.has("color"):
437
+ var c: Dictionary = mat_params["color"]
438
+ mat.color = Color(CommandParams.json_float(c, "r", 1), CommandParams.json_float(c, "g", 1), CommandParams.json_float(c, "b", 1), CommandParams.json_float(c, "a", 1))
439
+ if mat_params.has("scale_min"):
440
+ mat.scale_min = CommandParams.to_float(mat_params["scale_min"])
441
+ if mat_params.has("scale_max"):
442
+ mat.scale_max = CommandParams.to_float(mat_params["scale_max"])
443
+
444
+ respond({
445
+ "success": true, "node_path": node_path,
446
+ "emitting": node.get("emitting"), "amount": node.get("amount"),
447
+ "lifetime": node.get("lifetime"), "one_shot": node.get("one_shot"),
448
+ "speed_scale": node.get("speed_scale")
449
+ })
450
+
451
+
452
+ # --- Create Animation ---
453
+ func _cmd_viewport(params: Dictionary) -> void:
454
+ var reader := CommandParams.new(params)
455
+ var action: String = reader.optional_enum("action", "create", ["create", "configure", "get"])
456
+ if params_invalid(reader):
457
+ return
458
+
459
+ match action:
460
+ "create":
461
+ var parent_path: String = params.get("parent_path", "/root")
462
+ var parent: Node = get_tree().root.get_node_or_null(parent_path)
463
+ if parent == null:
464
+ respond({"error": "Parent node not found: %s" % parent_path})
465
+ return
466
+ var viewport: SubViewport = SubViewport.new()
467
+ if params.has("width") and params.has("height"):
468
+ viewport.size = Vector2i(CommandParams.to_int(params["width"]), CommandParams.to_int(params["height"]))
469
+ if params.has("transparent_bg"):
470
+ viewport.transparent_bg = CommandParams.to_bool(params["transparent_bg"])
471
+ if params.has("msaa"):
472
+ viewport.msaa_2d = CommandParams.to_int(params["msaa"]) as Viewport.MSAA
473
+ viewport.msaa_3d = CommandParams.to_int(params["msaa"]) as Viewport.MSAA
474
+ var custom_name: String = CommandParams.json_string(params, "name")
475
+ if not custom_name.is_empty():
476
+ viewport.name = custom_name
477
+ var container: SubViewportContainer = SubViewportContainer.new()
478
+ container.add_child(viewport)
479
+ parent.add_child(container)
480
+ respond({"success": true, "action": "create", "viewport_path": str(viewport.get_path()), "container_path": str(container.get_path()), "size": variant_to_json(viewport.size)})
481
+ "configure":
482
+ var node_path: String = params.get("node_path", "")
483
+ if node_path.is_empty():
484
+ respond({"error": "node_path is required for configure"})
485
+ return
486
+ var vp: Node = get_tree().root.get_node_or_null(node_path)
487
+ if vp == null or not vp is SubViewport:
488
+ respond({"error": "SubViewport not found: %s" % node_path})
489
+ return
490
+ var sv: SubViewport = vp as SubViewport
491
+ if params.has("width") and params.has("height"):
492
+ sv.size = Vector2i(CommandParams.to_int(params["width"]), CommandParams.to_int(params["height"]))
493
+ if params.has("transparent_bg"):
494
+ sv.transparent_bg = CommandParams.to_bool(params["transparent_bg"])
495
+ if params.has("msaa"):
496
+ sv.msaa_2d = CommandParams.to_int(params["msaa"]) as Viewport.MSAA
497
+ sv.msaa_3d = CommandParams.to_int(params["msaa"]) as Viewport.MSAA
498
+ respond({"success": true, "action": "configure", "size": variant_to_json(sv.size), "transparent_bg": sv.transparent_bg})
499
+ "get":
500
+ var node_path: String = params.get("node_path", "")
501
+ if node_path.is_empty():
502
+ respond({"error": "node_path is required for get"})
503
+ return
504
+ var vp: Node = get_tree().root.get_node_or_null(node_path)
505
+ if vp == null or not vp is SubViewport:
506
+ respond({"error": "SubViewport not found: %s" % node_path})
507
+ return
508
+ var sv: SubViewport = vp as SubViewport
509
+ respond({"success": true, "action": "get", "size": variant_to_json(sv.size), "transparent_bg": sv.transparent_bg, "msaa_2d": sv.msaa_2d, "msaa_3d": sv.msaa_3d})
510
+ _:
511
+ respond({"error": "Unknown viewport action: %s. Use create, configure, or get" % action})
512
+
513
+
514
+ # --- Debug Draw ---
515
+ var _debug_draw_node: Node = null
516
+ var _debug_meshes: Array = []
517
+
518
+ func _cmd_debug_draw(params: Dictionary) -> void:
519
+ var reader := CommandParams.new(params)
520
+ var action: String = reader.optional_enum("action", "line", ["line", "sphere", "box", "clear"])
521
+ var duration: int = reader.optional_int("duration", 0, 0)
522
+ if params_invalid(reader):
523
+ return
524
+ var color_dict: Dictionary = params.get("color", {"r": 1.0, "g": 0.0, "b": 0.0})
525
+ var color: Color = Color(CommandParams.json_float(color_dict, "r", 1), CommandParams.json_float(color_dict, "g", 0), CommandParams.json_float(color_dict, "b", 0), CommandParams.json_float(color_dict, "a", 1))
526
+ if action == "clear":
527
+ _clear_debug_draw()
528
+ respond({"success": true, "action": "clear"})
529
+ return
530
+
531
+ # Ensure we have a debug draw parent
532
+ if _debug_draw_node == null or not is_instance_valid(_debug_draw_node):
533
+ _debug_draw_node = Node3D.new()
534
+ _debug_draw_node.name = "_McpDebugDraw"
535
+ get_tree().root.add_child(_debug_draw_node)
536
+
537
+ var mat: StandardMaterial3D = StandardMaterial3D.new()
538
+ mat.albedo_color = color
539
+ mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
540
+ mat.no_depth_test = true
541
+ mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA if color.a < 1.0 else BaseMaterial3D.TRANSPARENCY_DISABLED
542
+
543
+ match action:
544
+ "line":
545
+ var from_dict: Dictionary = params.get("from", {})
546
+ var to_dict: Dictionary = params.get("to", {})
547
+ var from_pos: Vector3 = Vector3(CommandParams.json_float(from_dict, "x", 0), CommandParams.json_float(from_dict, "y", 0), CommandParams.json_float(from_dict, "z", 0))
548
+ var to_pos: Vector3 = Vector3(CommandParams.json_float(to_dict, "x", 0), CommandParams.json_float(to_dict, "y", 0), CommandParams.json_float(to_dict, "z", 0))
549
+ var im: ImmediateMesh = ImmediateMesh.new()
550
+ im.surface_begin(Mesh.PRIMITIVE_LINES, mat)
551
+ im.surface_add_vertex(from_pos)
552
+ im.surface_add_vertex(to_pos)
553
+ im.surface_end()
554
+ var mi: MeshInstance3D = MeshInstance3D.new()
555
+ mi.mesh = im
556
+ _debug_draw_node.add_child(mi)
557
+ _debug_meshes.append({"node": mi, "frames_left": duration})
558
+ respond({"success": true, "action": "line"})
559
+ "sphere":
560
+ var center_dict: Dictionary = params.get("center", {})
561
+ var center: Vector3 = Vector3(CommandParams.json_float(center_dict, "x", 0), CommandParams.json_float(center_dict, "y", 0), CommandParams.json_float(center_dict, "z", 0))
562
+ var radius: float = reader.optional_number("radius", 0.5, 0.0)
563
+ if params_invalid(reader):
564
+ return
565
+ if radius <= 0.0:
566
+ reader.fail("radius must be greater than zero", {"param": "radius", "reason": "out_of_range", "min_exclusive": 0})
567
+ send_params_error(reader)
568
+ return
569
+ var sphere_mesh: SphereMesh = SphereMesh.new()
570
+ sphere_mesh.radius = radius
571
+ sphere_mesh.height = radius * 2.0
572
+ sphere_mesh.material = mat
573
+ var mi: MeshInstance3D = MeshInstance3D.new()
574
+ mi.mesh = sphere_mesh
575
+ mi.global_position = center
576
+ _debug_draw_node.add_child(mi)
577
+ _debug_meshes.append({"node": mi, "frames_left": duration})
578
+ respond({"success": true, "action": "sphere"})
579
+ "box":
580
+ var center_dict: Dictionary = params.get("center", {})
581
+ var center: Vector3 = Vector3(CommandParams.json_float(center_dict, "x", 0), CommandParams.json_float(center_dict, "y", 0), CommandParams.json_float(center_dict, "z", 0))
582
+ var size_dict: Dictionary = params.get("size", {"x": 1, "y": 1, "z": 1})
583
+ var box_size: Vector3 = Vector3(CommandParams.json_float(size_dict, "x", 1), CommandParams.json_float(size_dict, "y", 1), CommandParams.json_float(size_dict, "z", 1))
584
+ var box_mesh: BoxMesh = BoxMesh.new()
585
+ box_mesh.size = box_size
586
+ box_mesh.material = mat
587
+ var mi: MeshInstance3D = MeshInstance3D.new()
588
+ mi.mesh = box_mesh
589
+ mi.global_position = center
590
+ _debug_draw_node.add_child(mi)
591
+ _debug_meshes.append({"node": mi, "frames_left": duration})
592
+ respond({"success": true, "action": "box"})
593
+ _:
594
+ respond({"error": "Unknown debug draw action: %s. Use line, sphere, box, or clear" % action})
595
+
596
+
597
+ func _cmd_gi(params: Dictionary) -> void:
598
+ var reader := CommandParams.new(params)
599
+ var parent: Node = require_node(reader, "parent_path", "/root")
600
+ var gi_type: String = reader.optional_enum("gi_type", "voxel_gi", ["voxel_gi", "lightmap_gi", "reflection_probe"])
601
+ if params_invalid(reader):
602
+ return
603
+ var node: VisualInstance3D
604
+ match gi_type:
605
+ "voxel_gi": node = VoxelGI.new()
606
+ "lightmap_gi": node = LightmapGI.new()
607
+ "reflection_probe": node = ReflectionProbe.new()
608
+ _:
609
+ respond({"error": "Unknown GI type: %s" % gi_type})
610
+ return
611
+ if params.has("size"):
612
+ var s: Dictionary = params["size"]
613
+ var extents := Vector3(CommandParams.json_float(s, "x", 10), CommandParams.json_float(s, "y", 10), CommandParams.json_float(s, "z", 10))
614
+ if node is VoxelGI:
615
+ (node as VoxelGI).size = extents
616
+ elif node is ReflectionProbe:
617
+ (node as ReflectionProbe).size = extents
618
+ var custom_name: String = CommandParams.json_string(params, "name")
619
+ if not custom_name.is_empty():
620
+ node.name = custom_name
621
+ parent.add_child(node)
622
+ respond({"success": true, "path": str(node.get_path()), "gi_type": gi_type})
623
+
624
+
625
+ func _cmd_sky(params: Dictionary) -> void:
626
+ var action: String = params.get("action", "create")
627
+ var env: Environment = _get_or_create_environment()
628
+ if env == null:
629
+ respond({"error": "Could not get or create environment"})
630
+ return
631
+ var sky_type: String = params.get("sky_type", "procedural")
632
+ if action == "create" or env.sky == null:
633
+ env.sky = Sky.new()
634
+ env.background_mode = Environment.BG_SKY
635
+ var sky_mat: ProceduralSkyMaterial = env.sky.sky_material as ProceduralSkyMaterial
636
+ if sky_mat == null:
637
+ sky_mat = ProceduralSkyMaterial.new()
638
+ if params.has("top_color"):
639
+ var c: Dictionary = params["top_color"]
640
+ sky_mat.sky_top_color = Color(CommandParams.json_float(c, "r", 0.4), CommandParams.json_float(c, "g", 0.6), CommandParams.json_float(c, "b", 1.0))
641
+ if params.has("bottom_color"):
642
+ var c: Dictionary = params["bottom_color"]
643
+ sky_mat.sky_horizon_color = Color(CommandParams.json_float(c, "r", 0.7), CommandParams.json_float(c, "g", 0.8), CommandParams.json_float(c, "b", 0.9))
644
+ if params.has("ground_color"):
645
+ var c: Dictionary = params["ground_color"]
646
+ sky_mat.ground_bottom_color = Color(CommandParams.json_float(c, "r", 0.1), CommandParams.json_float(c, "g", 0.1), CommandParams.json_float(c, "b", 0.1))
647
+ if params.has("sun_energy"):
648
+ sky_mat.sun_curve = CommandParams.to_float(params["sun_energy"])
649
+ env.sky.sky_material = sky_mat
650
+ respond({"success": true, "action": action, "sky_type": sky_type})
651
+
652
+
653
+ func _get_or_create_environment() -> Environment:
654
+ var cam: Camera3D = get_viewport().get_camera_3d()
655
+ if cam != null and cam.get_environment() != null:
656
+ return cam.get_environment()
657
+ var we: WorldEnvironment = null
658
+ for child in get_tree().root.get_children():
659
+ if child is WorldEnvironment:
660
+ we = child as WorldEnvironment
661
+ break
662
+ if we != null and we.environment != null:
663
+ return we.environment
664
+ # Create one
665
+ we = WorldEnvironment.new()
666
+ we.environment = Environment.new()
667
+ get_tree().root.add_child(we)
668
+ return we.environment
669
+
670
+
671
+ func _cmd_camera_attributes(params: Dictionary) -> void:
672
+ var reader := CommandParams.new(params)
673
+ var action: String = reader.optional_enum("action", "get", ["get", "set"])
674
+ if params_invalid(reader):
675
+ return
676
+ var cam: Camera3D = get_viewport().get_camera_3d()
677
+ if cam == null:
678
+ respond({"error": "No Camera3D found in viewport"})
679
+ return
680
+ if action == "get":
681
+ var current: CameraAttributesPractical = cam.attributes as CameraAttributesPractical
682
+ var info: Dictionary = {"success": true, "action": "get", "has_attributes": cam.attributes != null}
683
+ # Report the values, not just their presence: without them a client cannot
684
+ # confirm that a preceding `set` actually landed.
685
+ if current != null:
686
+ info["dof_blur_far"] = current.dof_blur_far_distance
687
+ info["dof_blur_near"] = current.dof_blur_near_distance
688
+ info["dof_blur_amount"] = current.dof_blur_amount
689
+ info["exposure_multiplier"] = current.exposure_multiplier
690
+ info["auto_exposure"] = current.auto_exposure_enabled
691
+ info["auto_exposure_scale"] = current.auto_exposure_scale
692
+ respond(info)
693
+ return
694
+ # set
695
+ if cam.attributes == null:
696
+ cam.attributes = CameraAttributesPractical.new()
697
+ var attr: CameraAttributesPractical = cam.attributes as CameraAttributesPractical
698
+ if attr == null:
699
+ respond({"error": "Camera attributes is not CameraAttributesPractical"})
700
+ return
701
+ if params.has("dof_blur_far"):
702
+ attr.dof_blur_far_enabled = true
703
+ attr.dof_blur_far_distance = CommandParams.to_float(params["dof_blur_far"])
704
+ if params.has("dof_blur_near"):
705
+ attr.dof_blur_near_enabled = true
706
+ attr.dof_blur_near_distance = CommandParams.to_float(params["dof_blur_near"])
707
+ if params.has("dof_blur_amount"):
708
+ attr.dof_blur_amount = CommandParams.to_float(params["dof_blur_amount"])
709
+ if params.has("exposure_multiplier"):
710
+ attr.exposure_multiplier = CommandParams.to_float(params["exposure_multiplier"])
711
+ if params.has("auto_exposure"):
712
+ attr.auto_exposure_enabled = CommandParams.to_bool(params["auto_exposure"])
713
+ if params.has("auto_exposure_scale"):
714
+ attr.auto_exposure_scale = CommandParams.to_float(params["auto_exposure_scale"])
715
+ respond({"success": true, "action": "set"})
716
+
717
+
718
+ # ==========================================================================
719
+ # Batch 3: Animation Advanced + Audio Effects
720
+ # ==========================================================================
721
+
722
+ func _cmd_render_settings(params: Dictionary) -> void:
723
+ var vp: Viewport = get_viewport()
724
+ var reader := CommandParams.new(params)
725
+ var action: String = reader.optional_enum("action", "get", ["get", "set"])
726
+ if params_invalid(reader):
727
+ return
728
+ if action == "get":
729
+ respond({"success": true, "msaa_2d": vp.msaa_2d, "msaa_3d": vp.msaa_3d, "screen_space_aa": vp.screen_space_aa, "use_taa": vp.use_taa, "scaling_3d_mode": vp.scaling_3d_mode, "scaling_3d_scale": vp.scaling_3d_scale})
730
+ return
731
+ if params.has("msaa_2d"):
732
+ vp.msaa_2d = CommandParams.to_int(params["msaa_2d"]) as Viewport.MSAA
733
+ if params.has("msaa_3d"):
734
+ vp.msaa_3d = CommandParams.to_int(params["msaa_3d"]) as Viewport.MSAA
735
+ if params.has("fxaa"):
736
+ vp.screen_space_aa = Viewport.SCREEN_SPACE_AA_FXAA if CommandParams.to_bool(params["fxaa"]) else Viewport.SCREEN_SPACE_AA_DISABLED
737
+ if params.has("taa"):
738
+ vp.use_taa = CommandParams.to_bool(params["taa"])
739
+ if params.has("scaling_mode"):
740
+ vp.scaling_3d_mode = CommandParams.to_int(params["scaling_mode"]) as Viewport.Scaling3DMode
741
+ if params.has("scaling_scale"):
742
+ vp.scaling_3d_scale = CommandParams.to_float(params["scaling_scale"])
743
+ respond({"success": true, "action": "set"})
744
+
745
+
746
+ func _cmd_video(params: Dictionary) -> void:
747
+ var reader := CommandParams.new(params)
748
+ var action: String = reader.optional_enum("action", "play", ["create", "play", "pause", "resume", "stop", "seek", "get_status"])
749
+ if params_invalid(reader):
750
+ return
751
+ if action == "create":
752
+ var parent_path: String = params.get("parent_path", "/root")
753
+ var parent: Node = get_tree().root.get_node_or_null(parent_path)
754
+ if parent == null:
755
+ respond({"error": "Parent not found: %s" % parent_path})
756
+ return
757
+ var vp: VideoStreamPlayer = VideoStreamPlayer.new()
758
+ var video_path: String = params.get("video_path", "")
759
+ if not video_path.is_empty():
760
+ if not ResourceLoader.exists(video_path):
761
+ respond({"error": "Video resource not found: %s" % video_path})
762
+ return
763
+ var stream: Resource = ResourceLoader.load(video_path)
764
+ if not stream is VideoStream:
765
+ respond({"error": "Resource is not a VideoStream: %s" % video_path})
766
+ return
767
+ vp.stream = stream
768
+ if params.has("volume"):
769
+ vp.volume = CommandParams.to_float(params["volume"])
770
+ if params.has("autoplay"):
771
+ vp.autoplay = CommandParams.to_bool(params["autoplay"])
772
+ if params.has("loop") and "loop" in vp:
773
+ vp.set("loop", CommandParams.to_bool(params["loop"]))
774
+ var custom_name: String = CommandParams.json_string(params, "name")
775
+ if not custom_name.is_empty():
776
+ vp.name = custom_name
777
+ parent.add_child(vp)
778
+ if vp.autoplay:
779
+ vp.play()
780
+ respond({"success": true, "action": "create", "path": str(vp.get_path())})
781
+ return
782
+ var node_path: String = params.get("node_path", "")
783
+ var node: Node = get_tree().root.get_node_or_null(node_path)
784
+ if node == null or not node is VideoStreamPlayer:
785
+ respond({"error": "VideoStreamPlayer not found: %s" % node_path})
786
+ return
787
+ var player: VideoStreamPlayer = node as VideoStreamPlayer
788
+ match action:
789
+ "play":
790
+ player.play()
791
+ respond({"success": true, "action": "play"})
792
+ "pause":
793
+ player.paused = true
794
+ respond({"success": true, "action": "pause"})
795
+ "resume":
796
+ player.paused = false
797
+ respond({"success": true, "action": "resume"})
798
+ "stop":
799
+ player.stop()
800
+ respond({"success": true, "action": "stop"})
801
+ "seek":
802
+ player.stream_position = CommandParams.json_float(params, "position", 0.0)
803
+ respond({"success": true, "action": "seek", "position": player.stream_position})
804
+ "get_status":
805
+ respond({"success": true, "action": "get_status", "is_playing": player.is_playing(), "paused": player.paused, "position": player.stream_position, "length": player.get_stream_length()})
806
+ _:
807
+ respond({"error": "Unknown video action: %s" % action})