@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,573 @@
1
+ extends "res://mcp_runtime/runtime_domain.gd"
2
+
3
+ # 3D scene and geometry domain: CSG, instanced and procedural meshes, lights,
4
+ # GridMap editing, scene effects, paths, and the built-in terrain mesh service.
5
+
6
+ func register_commands() -> void:
7
+ register_command("csg", _cmd_csg)
8
+ register_command("multimesh", _cmd_multimesh)
9
+ register_command("procedural_mesh", _cmd_procedural_mesh)
10
+ register_command("light_3d", _cmd_light_3d)
11
+ register_command("mesh_instance", _cmd_mesh_instance)
12
+ register_command("gridmap", _cmd_gridmap)
13
+ register_command("3d_effects", _cmd_3d_effects)
14
+ register_command("path_3d", _cmd_path_3d)
15
+ register_command("terrain", _cmd_terrain)
16
+
17
+
18
+ func _cmd_csg(params: Dictionary) -> void:
19
+ var reader := CommandParams.new(params)
20
+ var action: String = reader.optional_enum("action", "create", ["create", "configure"])
21
+ if params_invalid(reader):
22
+ return
23
+ if action == "create":
24
+ var parent: Node = require_node(reader, "parent_path", "/root")
25
+ var csg_type: String = reader.optional_enum("csg_type", "box", ["box", "sphere", "cylinder", "mesh", "combiner"])
26
+ var operation: String = reader.optional_enum("operation", "union", ["union", "intersection", "subtraction"])
27
+ if params_invalid(reader):
28
+ return
29
+ var node: CSGShape3D
30
+ match csg_type:
31
+ "box": node = CSGBox3D.new()
32
+ "sphere": node = CSGSphere3D.new()
33
+ "cylinder": node = CSGCylinder3D.new()
34
+ "mesh": node = CSGMesh3D.new()
35
+ "combiner": node = CSGCombiner3D.new()
36
+ if params.has("operation"):
37
+ match operation:
38
+ "union": node.operation = CSGShape3D.OPERATION_UNION
39
+ "intersection": node.operation = CSGShape3D.OPERATION_INTERSECTION
40
+ "subtraction": node.operation = CSGShape3D.OPERATION_SUBTRACTION
41
+ var custom_name: String = CommandParams.json_string(params, "name")
42
+ if not custom_name.is_empty():
43
+ node.name = custom_name
44
+ if node is CSGBox3D and params.has("size"):
45
+ var box_size: Variant = json_to_variant(params["size"], "Vector3")
46
+ if box_size is Vector3:
47
+ (node as CSGBox3D).size = box_size
48
+ if node is CSGSphere3D and params.has("radius"):
49
+ (node as CSGSphere3D).radius = CommandParams.to_float(params["radius"])
50
+ if node is CSGCylinder3D:
51
+ if params.has("radius"):
52
+ (node as CSGCylinder3D).radius = CommandParams.to_float(params["radius"])
53
+ if params.has("height"):
54
+ (node as CSGCylinder3D).height = CommandParams.to_float(params["height"])
55
+ if params.has("material"):
56
+ var material_path: String = reader.optional_string("material")
57
+ if not ResourceLoader.exists(material_path):
58
+ reader.fail("Material resource not found: %s" % material_path, {"param": "material", "reason": "resource_not_found", "value": material_path})
59
+ elif not "material" in node:
60
+ reader.fail("material is not supported by this CSG shape", {"param": "material", "reason": "invalid_value", "csg_type": csg_type})
61
+ else:
62
+ var material_resource: Resource = ResourceLoader.load(material_path)
63
+ if not material_resource is Material:
64
+ reader.fail("Resource is not a Material: %s" % material_path, {"param": "material", "reason": "invalid_value", "value": material_path})
65
+ else:
66
+ node.set("material", material_resource)
67
+ if params_invalid(reader):
68
+ node.free()
69
+ return
70
+ parent.add_child(node)
71
+ node.owner = get_tree().edited_scene_root if get_tree().edited_scene_root else get_tree().root
72
+ respond({"success": true, "action": "create", "path": str(node.get_path()), "type": csg_type})
73
+ elif action == "configure":
74
+ var node: Node = require_node(reader)
75
+ var operation: String = reader.optional_enum("operation", "union", ["union", "intersection", "subtraction"])
76
+ if node != null and not node is CSGShape3D:
77
+ reader.fail("node_path must reference a CSGShape3D", {"param": "node_path", "reason": "invalid_value", "expected_class": "CSGShape3D"})
78
+ if params_invalid(reader):
79
+ return
80
+ if params.has("operation"):
81
+ match operation:
82
+ "union": (node as CSGShape3D).operation = CSGShape3D.OPERATION_UNION
83
+ "intersection": (node as CSGShape3D).operation = CSGShape3D.OPERATION_INTERSECTION
84
+ "subtraction": (node as CSGShape3D).operation = CSGShape3D.OPERATION_SUBTRACTION
85
+ respond({"success": true, "action": "configure", "path": str(node.get_path())})
86
+
87
+
88
+ func _cmd_multimesh(params: Dictionary) -> void:
89
+ var action: String = params.get("action", "create")
90
+ match action:
91
+ "create":
92
+ var parent_path: String = params.get("parent_path", "/root")
93
+ var parent: Node = get_tree().root.get_node_or_null(parent_path)
94
+ if parent == null:
95
+ respond({"error": "Parent not found: %s" % parent_path})
96
+ return
97
+ var mmi: MultiMeshInstance3D = MultiMeshInstance3D.new()
98
+ var mm: MultiMesh = MultiMesh.new()
99
+ mm.transform_format = MultiMesh.TRANSFORM_3D
100
+ mm.instance_count = CommandParams.json_int(params, "count", 1)
101
+ var mesh_type: String = params.get("mesh_type", "box")
102
+ match mesh_type:
103
+ "box": mm.mesh = BoxMesh.new()
104
+ "sphere": mm.mesh = SphereMesh.new()
105
+ "cylinder": mm.mesh = CylinderMesh.new()
106
+ _: mm.mesh = BoxMesh.new()
107
+ mmi.multimesh = mm
108
+ var custom_name: String = CommandParams.json_string(params, "name")
109
+ if not custom_name.is_empty():
110
+ mmi.name = custom_name
111
+ parent.add_child(mmi)
112
+ respond({"success": true, "action": "create", "path": str(mmi.get_path()), "count": mm.instance_count})
113
+ "set_instance":
114
+ var node_path: String = params.get("node_path", "")
115
+ var node: Node = get_tree().root.get_node_or_null(node_path)
116
+ if node == null or not node is MultiMeshInstance3D:
117
+ respond({"error": "MultiMeshInstance3D not found: %s" % node_path})
118
+ return
119
+ var idx: int = CommandParams.json_int(params, "index", 0)
120
+ var mm: MultiMesh = (node as MultiMeshInstance3D).multimesh
121
+ if idx < 0 or idx >= mm.instance_count:
122
+ respond({"error": "index %d is outside the instance range 0..%d" % [idx, mm.instance_count - 1], "error_data": {"param": "index", "reason": "out_of_range", "value": idx, "instance_count": mm.instance_count}})
123
+ return
124
+ # Instance transforms live in the rendering server's buffer. Godot's
125
+ # headless dummy renderer allocates no buffer, so the write would be
126
+ # dropped without a word; say so instead of reporting success.
127
+ if not _multimesh_instance_data_available(mm):
128
+ respond_limit(
129
+ "MultiMesh instance data is unavailable: the active rendering server does not allocate instance buffers (Godot's headless dummy renderer). Run the game with a display to position MultiMesh instances.",
130
+ {"reason": "instance_buffer_unavailable", "video_adapter": RenderingServer.get_video_adapter_name()},
131
+ )
132
+ return
133
+ var tf: Dictionary = params.get("transform", {})
134
+ var origin: Dictionary = tf.get("origin", {})
135
+ var xform: Transform3D = Transform3D.IDENTITY
136
+ xform.origin = Vector3(CommandParams.json_float(origin, "x", 0), CommandParams.json_float(origin, "y", 0), CommandParams.json_float(origin, "z", 0))
137
+ mm.set_instance_transform(idx, xform)
138
+ respond({"success": true, "action": "set_instance", "index": idx})
139
+ "get_info":
140
+ var node_path: String = params.get("node_path", "")
141
+ var node: Node = get_tree().root.get_node_or_null(node_path)
142
+ if node == null or not node is MultiMeshInstance3D:
143
+ respond({"error": "MultiMeshInstance3D not found: %s" % node_path})
144
+ return
145
+ var mm: MultiMesh = (node as MultiMeshInstance3D).multimesh
146
+ respond({
147
+ "success": true,
148
+ "count": mm.instance_count if mm else 0,
149
+ "visible_count": mm.visible_instance_count if mm else 0,
150
+ "instance_data_available": _multimesh_instance_data_available(mm) if mm else false,
151
+ })
152
+ _:
153
+ respond({"error": "Unknown multimesh action: %s" % action})
154
+
155
+
156
+ # An instance_count > 0 with an empty buffer means the rendering server kept no
157
+ # per-instance storage, which is what the headless dummy renderer does.
158
+ func _multimesh_instance_data_available(mm: MultiMesh) -> bool:
159
+ return mm.instance_count <= 0 or not mm.buffer.is_empty()
160
+
161
+
162
+ # Mesh buffers arrive as [x, y, z] triples; curve points arrive as {x, y, z}.
163
+ func _vec3_from_triple(value: Variant) -> Vector3:
164
+ var triple: Array = CommandParams.as_array(value)
165
+ return Vector3(
166
+ CommandParams.to_float(triple[0] if triple.size() > 0 else 0.0),
167
+ CommandParams.to_float(triple[1] if triple.size() > 1 else 0.0),
168
+ CommandParams.to_float(triple[2] if triple.size() > 2 else 0.0),
169
+ )
170
+
171
+
172
+ func _vec3_from_object(value: Variant) -> Vector3:
173
+ var point: Dictionary = CommandParams.as_dictionary(value)
174
+ return Vector3(CommandParams.json_float(point, "x"), CommandParams.json_float(point, "y"), CommandParams.json_float(point, "z"))
175
+
176
+
177
+ func _cmd_procedural_mesh(params: Dictionary) -> void:
178
+ var parent_path: String = params.get("parent_path", "/root")
179
+ var parent: Node = get_tree().root.get_node_or_null(parent_path)
180
+ if parent == null:
181
+ respond({"error": "Parent not found: %s" % parent_path})
182
+ return
183
+ var verts: PackedVector3Array = PackedVector3Array()
184
+ for v: Variant in CommandParams.json_array(params, "vertices"):
185
+ @warning_ignore("return_value_discarded")
186
+ verts.append(_vec3_from_triple(v))
187
+ var arrays: Array = []
188
+ @warning_ignore("return_value_discarded")
189
+ arrays.resize(Mesh.ARRAY_MAX)
190
+ arrays[Mesh.ARRAY_VERTEX] = verts
191
+ if params.has("normals"):
192
+ var norms: PackedVector3Array = PackedVector3Array()
193
+ for n: Variant in CommandParams.json_array(params, "normals"):
194
+ @warning_ignore("return_value_discarded")
195
+ norms.append(_vec3_from_triple(n))
196
+ arrays[Mesh.ARRAY_NORMAL] = norms
197
+ if params.has("uvs"):
198
+ var uvs: PackedVector2Array = PackedVector2Array()
199
+ for uv: Variant in CommandParams.json_array(params, "uvs"):
200
+ var pair: Array = CommandParams.as_array(uv)
201
+ @warning_ignore("return_value_discarded")
202
+ uvs.append(Vector2(CommandParams.to_float(pair[0] if pair.size() > 0 else 0.0), CommandParams.to_float(pair[1] if pair.size() > 1 else 0.0)))
203
+ arrays[Mesh.ARRAY_TEX_UV] = uvs
204
+ if params.has("indices"):
205
+ var indices: PackedInt32Array = PackedInt32Array()
206
+ for idx: Variant in CommandParams.json_array(params, "indices"):
207
+ @warning_ignore("return_value_discarded")
208
+ indices.append(CommandParams.to_int(idx))
209
+ arrays[Mesh.ARRAY_INDEX] = indices
210
+ var mesh: ArrayMesh = ArrayMesh.new()
211
+ mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
212
+ var mi: MeshInstance3D = MeshInstance3D.new()
213
+ mi.mesh = mesh
214
+ var custom_name: String = CommandParams.json_string(params, "name")
215
+ if not custom_name.is_empty():
216
+ mi.name = custom_name
217
+ parent.add_child(mi)
218
+ respond({"success": true, "path": str(mi.get_path()), "vertex_count": verts.size()})
219
+
220
+
221
+ func _cmd_light_3d(params: Dictionary) -> void:
222
+ var action: String = params.get("action", "create")
223
+ if action == "create":
224
+ var parent_path: String = params.get("parent_path", "/root")
225
+ var parent: Node = get_tree().root.get_node_or_null(parent_path)
226
+ if parent == null:
227
+ respond({"error": "Parent not found: %s" % parent_path})
228
+ return
229
+ var light_type: String = params.get("light_type", "omni")
230
+ var light: Light3D
231
+ match light_type:
232
+ "directional": light = DirectionalLight3D.new()
233
+ "omni": light = OmniLight3D.new()
234
+ "spot": light = SpotLight3D.new()
235
+ _:
236
+ respond({"error": "Unknown light type: %s" % light_type})
237
+ return
238
+ if params.has("color"):
239
+ var c: Dictionary = params["color"]
240
+ light.light_color = Color(CommandParams.json_float(c, "r", 1), CommandParams.json_float(c, "g", 1), CommandParams.json_float(c, "b", 1))
241
+ if params.has("energy"):
242
+ light.light_energy = CommandParams.to_float(params["energy"])
243
+ if params.has("shadows"):
244
+ light.shadow_enabled = CommandParams.to_bool(params["shadows"])
245
+ if light is OmniLight3D and params.has("range"):
246
+ (light as OmniLight3D).omni_range = CommandParams.to_float(params["range"])
247
+ if light is SpotLight3D:
248
+ if params.has("range"):
249
+ (light as SpotLight3D).spot_range = CommandParams.to_float(params["range"])
250
+ if params.has("spot_angle"):
251
+ (light as SpotLight3D).spot_angle = CommandParams.to_float(params["spot_angle"])
252
+ var custom_name: String = CommandParams.json_string(params, "name")
253
+ if not custom_name.is_empty():
254
+ light.name = custom_name
255
+ parent.add_child(light)
256
+ respond({"success": true, "action": "create", "path": str(light.get_path()), "type": light_type})
257
+ elif action == "configure":
258
+ var node_path: String = params.get("node_path", "")
259
+ var node: Node = get_tree().root.get_node_or_null(node_path)
260
+ if node == null or not node is Light3D:
261
+ respond({"error": "Light3D not found: %s" % node_path})
262
+ return
263
+ var light: Light3D = node as Light3D
264
+ if params.has("color"):
265
+ var c: Dictionary = params["color"]
266
+ light.light_color = Color(CommandParams.json_float(c, "r", 1), CommandParams.json_float(c, "g", 1), CommandParams.json_float(c, "b", 1))
267
+ if params.has("energy"):
268
+ light.light_energy = CommandParams.to_float(params["energy"])
269
+ if params.has("shadows"):
270
+ light.shadow_enabled = CommandParams.to_bool(params["shadows"])
271
+ respond({"success": true, "action": "configure", "path": str(node.get_path())})
272
+ else:
273
+ respond({"error": "Unknown light_3d action: %s" % action})
274
+
275
+
276
+ func _cmd_mesh_instance(params: Dictionary) -> void:
277
+ var reader := CommandParams.new(params)
278
+ var parent: Node = require_node(reader, "parent_path", "/root")
279
+ var mesh_type: String = reader.optional_enum("mesh_type", "box", ["box", "sphere", "cylinder", "capsule", "plane", "quad"])
280
+ var radius: float = reader.optional_number("radius", 0.0, 0.0)
281
+ var height: float = reader.optional_number("height", 0.0, 0.0)
282
+ if params_invalid(reader):
283
+ return
284
+ var mesh: Mesh
285
+ match mesh_type:
286
+ "box": mesh = BoxMesh.new()
287
+ "sphere": mesh = SphereMesh.new()
288
+ "cylinder": mesh = CylinderMesh.new()
289
+ "capsule": mesh = CapsuleMesh.new()
290
+ "plane": mesh = PlaneMesh.new()
291
+ "quad": mesh = QuadMesh.new()
292
+ if params.has("size"):
293
+ var s: Dictionary = reader.required_dictionary("size")
294
+ if mesh is BoxMesh:
295
+ (mesh as BoxMesh).size = Vector3(CommandParams.json_float(s, "x", 1), CommandParams.json_float(s, "y", 1), CommandParams.json_float(s, "z", 1))
296
+ elif mesh is QuadMesh:
297
+ (mesh as QuadMesh).size = Vector2(CommandParams.json_float(s, "x", 1), CommandParams.json_float(s, "y", 1))
298
+ elif mesh is PlaneMesh:
299
+ (mesh as PlaneMesh).size = Vector2(CommandParams.json_float(s, "x", 1), CommandParams.json_float(s, "z", 1))
300
+ if params_invalid(reader):
301
+ return
302
+ if params.has("radius"):
303
+ if mesh is SphereMesh: (mesh as SphereMesh).radius = radius
304
+ elif mesh is CylinderMesh:
305
+ (mesh as CylinderMesh).top_radius = radius
306
+ (mesh as CylinderMesh).bottom_radius = radius
307
+ elif mesh is CapsuleMesh: (mesh as CapsuleMesh).radius = radius
308
+ if params.has("height"):
309
+ if mesh is CylinderMesh: (mesh as CylinderMesh).height = height
310
+ elif mesh is CapsuleMesh: (mesh as CapsuleMesh).height = height
311
+ elif mesh is SphereMesh: (mesh as SphereMesh).height = height
312
+ var mi: MeshInstance3D = MeshInstance3D.new()
313
+ mi.mesh = mesh
314
+ if params.has("material") and params["material"] is String:
315
+ var mat: StandardMaterial3D = StandardMaterial3D.new()
316
+ var hex: String = params["material"]
317
+ if hex.begins_with("#") or hex.length() == 6 or hex.length() == 8:
318
+ mat.albedo_color = Color.from_string(hex, Color.WHITE)
319
+ mi.material_override = mat
320
+ var custom_name: String = CommandParams.json_string(params, "name")
321
+ if not custom_name.is_empty():
322
+ mi.name = custom_name
323
+ parent.add_child(mi)
324
+ respond({"success": true, "path": str(mi.get_path()), "mesh_type": mesh_type})
325
+
326
+
327
+ func _cmd_gridmap(params: Dictionary) -> void:
328
+ var node_path: String = params.get("node_path", "")
329
+ var node: Node = get_tree().root.get_node_or_null(node_path)
330
+ if node == null or not node is GridMap:
331
+ respond({"error": "GridMap not found: %s" % node_path})
332
+ return
333
+ var gm: GridMap = node as GridMap
334
+ var action: String = params.get("action", "get_used")
335
+ match action:
336
+ "set_cell":
337
+ gm.set_cell_item(Vector3i(CommandParams.json_int(params, "x", 0), CommandParams.json_int(params, "y", 0), CommandParams.json_int(params, "z", 0)), CommandParams.json_int(params, "item", 0), CommandParams.json_int(params, "orientation", 0))
338
+ respond({"success": true, "action": "set_cell"})
339
+ "get_cell":
340
+ var item: int = gm.get_cell_item(Vector3i(CommandParams.json_int(params, "x", 0), CommandParams.json_int(params, "y", 0), CommandParams.json_int(params, "z", 0)))
341
+ respond({"success": true, "action": "get_cell", "item": item})
342
+ "clear":
343
+ gm.clear()
344
+ respond({"success": true, "action": "clear"})
345
+ "get_used":
346
+ var cells: Array = gm.get_used_cells()
347
+ var result: Array = []
348
+ for c: Vector3i in cells.slice(0, 100):
349
+ result.append({"x": c.x, "y": c.y, "z": c.z})
350
+ respond({"success": true, "action": "get_used", "cells": result, "total": cells.size()})
351
+ _:
352
+ respond({"error": "Unknown gridmap action: %s" % action})
353
+
354
+
355
+ func _cmd_3d_effects(params: Dictionary) -> void:
356
+ var parent_path: String = params.get("parent_path", "/root")
357
+ var parent: Node = get_tree().root.get_node_or_null(parent_path)
358
+ if parent == null:
359
+ respond({"error": "Parent not found: %s" % parent_path})
360
+ return
361
+ var effect_type: String = params.get("effect_type", "")
362
+ var node: Node3D
363
+ match effect_type:
364
+ "reflection_probe": node = ReflectionProbe.new()
365
+ "decal": node = Decal.new()
366
+ "fog_volume": node = FogVolume.new()
367
+ _:
368
+ respond({"error": "Unknown effect type: %s" % effect_type})
369
+ return
370
+ if params.has("size"):
371
+ var s: Dictionary = params["size"]
372
+ var size_v: Vector3 = Vector3(CommandParams.json_float(s, "x", 1), CommandParams.json_float(s, "y", 1), CommandParams.json_float(s, "z", 1))
373
+ if node is ReflectionProbe: (node as ReflectionProbe).size = size_v
374
+ elif node is Decal: (node as Decal).size = size_v
375
+ elif node is FogVolume: (node as FogVolume).size = size_v
376
+ if params.has("intensity"):
377
+ var intensity: float = CommandParams.to_float(params["intensity"])
378
+ if node is ReflectionProbe:
379
+ (node as ReflectionProbe).intensity = intensity
380
+ elif node is Decal:
381
+ (node as Decal).albedo_mix = intensity
382
+ elif node is FogVolume:
383
+ var fog_material: FogMaterial = FogMaterial.new()
384
+ fog_material.density = intensity
385
+ (node as FogVolume).material = fog_material
386
+ var custom_name: String = CommandParams.json_string(params, "name")
387
+ if not custom_name.is_empty():
388
+ node.name = custom_name
389
+ parent.add_child(node)
390
+ respond({"success": true, "path": str(node.get_path()), "effect_type": effect_type})
391
+
392
+
393
+ func _cmd_path_3d(params: Dictionary) -> void:
394
+ var reader := CommandParams.new(params)
395
+ var action: String = reader.optional_enum("action", "create", ["create", "add_point", "get_points", "set_points"])
396
+ if params_invalid(reader):
397
+ return
398
+ match action:
399
+ "create":
400
+ var parent: Node = require_node(reader, "parent_path", "/root")
401
+ if params_invalid(reader):
402
+ return
403
+ var path_node: Path3D = Path3D.new()
404
+ path_node.curve = Curve3D.new()
405
+ var custom_name: String = CommandParams.json_string(params, "name")
406
+ if not custom_name.is_empty():
407
+ path_node.name = custom_name
408
+ if params.has("points"):
409
+ for p: Variant in CommandParams.json_array(params, "points"):
410
+ path_node.curve.add_point(_vec3_from_object(p))
411
+ parent.add_child(path_node)
412
+ respond({"success": true, "action": "create", "path": str(path_node.get_path()), "point_count": path_node.curve.point_count})
413
+ "add_point":
414
+ var node_path: String = reader.required_node_path()
415
+ var point: Dictionary = reader.required_dictionary("point")
416
+ var node: Node = get_tree().root.get_node_or_null(node_path) if not reader.failed() else null
417
+ if params_invalid(reader):
418
+ return
419
+ if node == null or not node is Path3D:
420
+ respond({"error": "Path3D not found: %s" % node_path})
421
+ return
422
+ (node as Path3D).curve.add_point(_vec3_from_object(point))
423
+ respond({"success": true, "action": "add_point", "point_count": (node as Path3D).curve.point_count})
424
+ "get_points":
425
+ var node_path: String = params.get("node_path", "")
426
+ var node: Node = get_tree().root.get_node_or_null(node_path)
427
+ if node == null or not node is Path3D:
428
+ respond({"error": "Path3D not found: %s" % node_path})
429
+ return
430
+ var pts: Array = []
431
+ for i in (node as Path3D).curve.point_count:
432
+ var pt: Vector3 = (node as Path3D).curve.get_point_position(i)
433
+ pts.append({"x": pt.x, "y": pt.y, "z": pt.z})
434
+ respond({"success": true, "action": "get_points", "points": pts})
435
+ "set_points":
436
+ var node_path: String = reader.required_node_path()
437
+ var points: Array = reader.required_array("points")
438
+ var node: Node = get_tree().root.get_node_or_null(node_path) if not reader.failed() else null
439
+ if params_invalid(reader):
440
+ return
441
+ if node == null or not node is Path3D:
442
+ respond({"error": "Path3D not found: %s" % node_path})
443
+ return
444
+ var curve: Curve3D = (node as Path3D).curve
445
+ if curve == null:
446
+ curve = Curve3D.new()
447
+ (node as Path3D).curve = curve
448
+ curve.clear_points()
449
+ for p: Variant in points:
450
+ curve.add_point(_vec3_from_object(p))
451
+ respond({"success": true, "action": "set_points", "point_count": curve.point_count})
452
+ _:
453
+ respond({"error": "Unknown path_3d action: %s" % action})
454
+
455
+
456
+ func _cmd_terrain(params: Dictionary) -> void:
457
+ var reader := CommandParams.new(params)
458
+ var action: String = reader.optional_enum("action", "create", ["create", "get_height", "modify", "paint"])
459
+ if params_invalid(reader):
460
+ return
461
+ if action == "create":
462
+ var parent: Node = require_node(reader, "parent_path", "/root")
463
+ var width: int = reader.optional_int("width", 16, 2)
464
+ var depth: int = reader.optional_int("depth", 16, 2)
465
+ var max_height: float = reader.optional_number("max_height", 1.0)
466
+ var height_data: Array = reader.optional_array("height_data")
467
+ if params_invalid(reader):
468
+ return
469
+ var heights: Array = []
470
+ for i in range(width * depth):
471
+ var h: float = CommandParams.to_float(height_data[i]) * max_height if i < height_data.size() else 0.0
472
+ heights.append(h)
473
+ var colors: Array = []
474
+ for i in range(width * depth):
475
+ colors.append(Color.WHITE)
476
+ var mi: MeshInstance3D = MeshInstance3D.new()
477
+ var custom_name: String = CommandParams.json_string(params, "name")
478
+ if not custom_name.is_empty():
479
+ mi.name = custom_name
480
+ mi.set_meta("terrain_width", width)
481
+ mi.set_meta("terrain_depth", depth)
482
+ mi.set_meta("terrain_heights", heights)
483
+ mi.set_meta("terrain_colors", colors)
484
+ parent.add_child(mi)
485
+ _terrain_rebuild(mi)
486
+ respond({"success": true, "action": "create", "path": str(mi.get_path()), "width": width, "depth": depth})
487
+ return
488
+ var node_path: String = reader.required_node_path()
489
+ var node: Node = get_tree().root.get_node_or_null(node_path) if not reader.failed() else null
490
+ if params_invalid(reader):
491
+ return
492
+ if node == null or not node is MeshInstance3D or not node.has_meta("terrain_width"):
493
+ respond({"error": "Terrain node not found: %s" % node_path})
494
+ return
495
+ var mesh_node: MeshInstance3D = node as MeshInstance3D
496
+ var t_width: int = mesh_node.get_meta("terrain_width")
497
+ var t_depth: int = mesh_node.get_meta("terrain_depth")
498
+ var t_heights: Array = mesh_node.get_meta("terrain_heights")
499
+ var t_colors: Array = mesh_node.get_meta("terrain_colors")
500
+ match action:
501
+ "get_height":
502
+ var gx: int = reader.required_int("x")
503
+ var gz: int = reader.required_int("z")
504
+ if params_invalid(reader):
505
+ return
506
+ if gx < 0 or gx >= t_width or gz < 0 or gz >= t_depth:
507
+ respond({"error": "Coordinate out of bounds"})
508
+ return
509
+ respond({"success": true, "action": "get_height", "x": gx, "z": gz, "height": t_heights[gz * t_width + gx]})
510
+ "modify":
511
+ var cx: float = reader.required_number("x")
512
+ var cz: float = reader.required_number("z")
513
+ var radius: float = reader.required_number("radius", 0.0)
514
+ var delta: float = reader.required_number("height_delta")
515
+ if params_invalid(reader):
516
+ return
517
+ for z in range(t_depth):
518
+ for x in range(t_width):
519
+ var d: float = Vector2(x - cx, z - cz).length()
520
+ if d <= radius:
521
+ var falloff: float = 1.0 - (d / radius) if radius > 0.0 else 1.0
522
+ t_heights[z * t_width + x] += delta * falloff
523
+ mesh_node.set_meta("terrain_heights", t_heights)
524
+ _terrain_rebuild(mesh_node)
525
+ respond({"success": true, "action": "modify"})
526
+ "paint":
527
+ var cx: float = reader.required_number("x")
528
+ var cz: float = reader.required_number("z")
529
+ var radius: float = reader.required_number("radius", 0.0)
530
+ var col_d: Dictionary = reader.required_dictionary("color")
531
+ if params_invalid(reader):
532
+ return
533
+ var col: Color = Color(CommandParams.json_float(col_d, "r", 1), CommandParams.json_float(col_d, "g", 1), CommandParams.json_float(col_d, "b", 1), CommandParams.json_float(col_d, "a", 1))
534
+ for z in range(t_depth):
535
+ for x in range(t_width):
536
+ if Vector2(x - cx, z - cz).length() <= radius:
537
+ t_colors[z * t_width + x] = col
538
+ mesh_node.set_meta("terrain_colors", t_colors)
539
+ _terrain_rebuild(mesh_node)
540
+ respond({"success": true, "action": "paint"})
541
+ _:
542
+ respond({"error": "Unknown terrain action: %s" % action})
543
+
544
+
545
+ func _terrain_rebuild(mi: MeshInstance3D) -> void:
546
+ var width: int = mi.get_meta("terrain_width")
547
+ var depth: int = mi.get_meta("terrain_depth")
548
+ var heights: Array = mi.get_meta("terrain_heights")
549
+ var colors: Array = mi.get_meta("terrain_colors")
550
+ var st: SurfaceTool = SurfaceTool.new()
551
+ st.begin(Mesh.PRIMITIVE_TRIANGLES)
552
+ for z in range(depth - 1):
553
+ for x in range(width - 1):
554
+ var i00: int = z * width + x
555
+ var i10: int = z * width + (x + 1)
556
+ var i01: int = (z + 1) * width + x
557
+ var i11: int = (z + 1) * width + (x + 1)
558
+ var v00: Vector3 = Vector3(x, CommandParams.to_float(heights[i00]), z)
559
+ var v10: Vector3 = Vector3(x + 1, CommandParams.to_float(heights[i10]), z)
560
+ var v01: Vector3 = Vector3(x, CommandParams.to_float(heights[i01]), z + 1)
561
+ var v11: Vector3 = Vector3(x + 1, CommandParams.to_float(heights[i11]), z + 1)
562
+ var triangles: Array[Array] = [[i00, v00], [i10, v10], [i01, v01], [i10, v10], [i11, v11], [i01, v01]]
563
+ for tri: Array in triangles:
564
+ var color_index: int = tri[0]
565
+ var vertex: Vector3 = tri[1]
566
+ var vertex_color: Color = colors[color_index]
567
+ st.set_color(vertex_color)
568
+ st.add_vertex(vertex)
569
+ st.generate_normals()
570
+ var mat: StandardMaterial3D = StandardMaterial3D.new()
571
+ mat.vertex_color_use_as_albedo = true
572
+ st.set_material(mat)
573
+ mi.mesh = st.commit()