@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,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()
|