@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,653 @@
1
+ extends "res://mcp_runtime/runtime_domain.gd"
2
+
3
+ # Physics and navigation domain: raycasts and direct-space queries in 2D and
4
+ # 3D, collision shapes, physics body properties, joints, NavigationRegion3D
5
+ # management, and NavigationServer path queries. Commands that accept both
6
+ # dimensions pick 2D or 3D from the parameters (a `z` component) or from the
7
+ # target node's class, exactly as before the move.
8
+
9
+ const SHAPE_TYPES_3D: Array = ["box", "sphere", "capsule", "cylinder", "ray"]
10
+ const SHAPE_TYPES_2D: Array = ["box", "circle", "capsule", "segment"]
11
+ const JOINT_TYPES: Array = ["pin_2d", "spring_2d", "groove_2d", "pin_3d", "hinge_3d", "cone_3d", "slider_3d"]
12
+
13
+
14
+ func register_commands() -> void:
15
+ register_command("raycast", _cmd_raycast)
16
+ register_command("navigate_path", _cmd_navigate_path)
17
+ register_command("add_collision", _cmd_add_collision)
18
+ register_command("physics_body", _cmd_physics_body)
19
+ register_command("create_joint", _cmd_create_joint)
20
+ register_command("navigation_3d", _cmd_navigation_3d)
21
+ register_command("physics_3d", _cmd_physics_3d)
22
+ register_command("physics_2d", _cmd_physics_2d)
23
+
24
+
25
+ # Records a structured failure when the resolved node is not the class the
26
+ # command drives; the caller still routes through params_invalid().
27
+ func _require_class(reader: CommandParams, node: Node, type_name: String) -> void:
28
+ if reader.failed() or node == null:
29
+ return
30
+ reader.fail("Node is not a %s: %s" % [type_name, node.get_class()],
31
+ {"param": "node_path", "reason": "invalid_value", "expected": type_name, "value": node.get_class()})
32
+
33
+
34
+ func _vector2_from(value: Variant) -> Vector2:
35
+ var source: Dictionary = CommandParams.as_dictionary(value)
36
+ return Vector2(CommandParams.json_float(source, "x"), CommandParams.json_float(source, "y"))
37
+
38
+
39
+ func _vector3_from(value: Variant) -> Vector3:
40
+ var source: Dictionary = CommandParams.as_dictionary(value)
41
+ return Vector3(CommandParams.json_float(source, "x"), CommandParams.json_float(source, "y"), CommandParams.json_float(source, "z"))
42
+
43
+
44
+ func _apply_optional_name(reader: CommandParams, node: Node) -> void:
45
+ var node_name: String = reader.optional_string("name")
46
+ if not node_name.is_empty():
47
+ node.name = node_name
48
+
49
+
50
+ # --- Raycast (2D or 3D depending on whether z is present) ---
51
+ func _cmd_raycast(params: Dictionary) -> void:
52
+ var reader: CommandParams = CommandParams.new(params)
53
+ var from_dict: Dictionary = reader.required_dictionary("from")
54
+ var to_dict: Dictionary = reader.required_dictionary("to")
55
+ var collision_mask: int = reader.optional_int("collision_mask", 0xFFFFFFFF)
56
+ if not reader.failed() and (from_dict.is_empty() or to_dict.is_empty()):
57
+ var empty_name: String = "from" if from_dict.is_empty() else "to"
58
+ reader.fail("%s must be a position object" % empty_name, {"param": empty_name, "reason": "invalid_value"})
59
+ if params_invalid(reader):
60
+ return
61
+
62
+ var is_3d: bool = from_dict.has("z") or to_dict.has("z")
63
+ # Wait a frame so the physics state reflects the latest scene changes.
64
+ await get_tree().process_frame
65
+
66
+ if is_3d:
67
+ var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(
68
+ _vector3_from(from_dict), _vector3_from(to_dict), collision_mask)
69
+ _respond_ray_hit("3d", get_viewport().world_3d.direct_space_state.intersect_ray(query))
70
+ else:
71
+ var query: PhysicsRayQueryParameters2D = PhysicsRayQueryParameters2D.create(
72
+ _vector2_from(from_dict), _vector2_from(to_dict), collision_mask)
73
+ _respond_ray_hit("2d", get_viewport().world_2d.direct_space_state.intersect_ray(query))
74
+
75
+
76
+ func _respond_ray_hit(mode: String, result: Dictionary) -> void:
77
+ if result.is_empty():
78
+ respond({"success": true, "hit": false, "mode": mode})
79
+ return
80
+ respond({
81
+ "success": true, "hit": true, "mode": mode,
82
+ "position": variant_to_json(result["position"]),
83
+ "normal": variant_to_json(result["normal"]),
84
+ "collider_path": _collider_path(result),
85
+ "collider_class": _collider_class(result),
86
+ })
87
+
88
+
89
+ func _collider(result: Dictionary) -> Node:
90
+ var collider: Variant = result.get("collider")
91
+ if collider is Node:
92
+ return collider
93
+ return null
94
+
95
+
96
+ func _collider_path(result: Dictionary) -> String:
97
+ var collider: Node = _collider(result)
98
+ return str(collider.get_path()) if collider != null else ""
99
+
100
+
101
+ func _collider_class(result: Dictionary) -> String:
102
+ var collider: Node = _collider(result)
103
+ return collider.get_class() if collider != null else ""
104
+
105
+
106
+ # --- Navigation path query via NavigationServer ---
107
+ func _cmd_navigate_path(params: Dictionary) -> void:
108
+ var reader: CommandParams = CommandParams.new(params)
109
+ var start: Dictionary = reader.required_dictionary("start")
110
+ var end: Dictionary = reader.required_dictionary("end")
111
+ var optimize: bool = reader.optional_bool("optimize", true)
112
+ if not reader.failed() and (start.is_empty() or end.is_empty()):
113
+ var empty_name: String = "start" if start.is_empty() else "end"
114
+ reader.fail("%s must be a position object" % empty_name, {"param": empty_name, "reason": "invalid_value"})
115
+ if params_invalid(reader):
116
+ return
117
+
118
+ # Wait a frame so the navigation map is ready.
119
+ await get_tree().process_frame
120
+
121
+ if start.has("z") or end.has("z"):
122
+ var map_rid: RID = get_tree().root.get_world_3d().get_navigation_map()
123
+ NavigationServer3D.map_force_update(map_rid)
124
+ var path: PackedVector3Array = NavigationServer3D.map_get_path(map_rid, _vector3_from(start), _vector3_from(end), optimize)
125
+ var total_length: float = 0.0
126
+ for i: int in range(1, path.size()):
127
+ total_length += path[i - 1].distance_to(path[i])
128
+ respond({"success": true, "mode": "3d", "path": variant_to_json(path), "point_count": path.size(), "total_length": total_length})
129
+ else:
130
+ var map_rid: RID = get_tree().root.get_world_2d().get_navigation_map()
131
+ NavigationServer2D.map_force_update(map_rid)
132
+ var path: PackedVector2Array = NavigationServer2D.map_get_path(map_rid, _vector2_from(start), _vector2_from(end), optimize)
133
+ var total_length: float = 0.0
134
+ for i: int in range(1, path.size()):
135
+ total_length += path[i - 1].distance_to(path[i])
136
+ respond({"success": true, "mode": "2d", "path": variant_to_json(path), "point_count": path.size(), "total_length": total_length})
137
+
138
+
139
+ # --- Collision shapes ---
140
+ func _cmd_add_collision(params: Dictionary) -> void:
141
+ var reader: CommandParams = CommandParams.new(params)
142
+ var parent: Node = require_node(reader, "parent_path")
143
+ var shape_type: String = reader.required_string("shape_type")
144
+ var shape_params: Dictionary = reader.optional_dictionary("shape_params")
145
+ var disabled: bool = reader.optional_bool("disabled", false)
146
+ var collision_layer: int = reader.optional_int("collision_layer", 0)
147
+ var collision_mask: int = reader.optional_int("collision_mask", 0)
148
+ if parent != null and not (parent is CollisionObject2D or parent is CollisionObject3D):
149
+ _require_class(reader, parent, "CollisionObject2D or CollisionObject3D")
150
+ var is_3d: bool = parent is CollisionObject3D
151
+ if not reader.failed():
152
+ var allowed: Array = SHAPE_TYPES_3D if is_3d else SHAPE_TYPES_2D
153
+ if not allowed.has(shape_type):
154
+ reader.fail("Unknown %s shape type: %s. Use %s" % ["3D" if is_3d else "2D", shape_type, ", ".join(allowed)],
155
+ {"param": "shape_type", "reason": "invalid_value", "allowed": allowed, "value": shape_type})
156
+ if params_invalid(reader):
157
+ return
158
+
159
+ var col_shape: Node
160
+ if is_3d:
161
+ var shape_3d: CollisionShape3D = CollisionShape3D.new()
162
+ shape_3d.shape = _shape_3d(shape_type, shape_params)
163
+ if reader.has_param("disabled"):
164
+ shape_3d.disabled = disabled
165
+ col_shape = shape_3d
166
+ else:
167
+ var shape_2d: CollisionShape2D = CollisionShape2D.new()
168
+ shape_2d.shape = _shape_2d(shape_type, shape_params)
169
+ if reader.has_param("disabled"):
170
+ shape_2d.disabled = disabled
171
+ col_shape = shape_2d
172
+ parent.add_child(col_shape)
173
+ col_shape.owner = get_tree().edited_scene_root if get_tree().edited_scene_root else get_tree().root
174
+ if reader.has_param("collision_layer"):
175
+ parent.set("collision_layer", collision_layer)
176
+ if reader.has_param("collision_mask"):
177
+ parent.set("collision_mask", collision_mask)
178
+ respond({"success": true, "name": col_shape.name, "path": str(col_shape.get_path()), "shape_type": shape_type, "mode": "3d" if is_3d else "2d"})
179
+
180
+
181
+ func _shape_3d(shape_type: String, shape_params: Dictionary) -> Shape3D:
182
+ match shape_type:
183
+ "box":
184
+ var s: BoxShape3D = BoxShape3D.new()
185
+ s.size = Vector3(CommandParams.json_float(shape_params, "size_x", 1), CommandParams.json_float(shape_params, "size_y", 1), CommandParams.json_float(shape_params, "size_z", 1))
186
+ return s
187
+ "sphere":
188
+ var s: SphereShape3D = SphereShape3D.new()
189
+ s.radius = CommandParams.json_float(shape_params, "radius", 0.5)
190
+ return s
191
+ "capsule":
192
+ var s: CapsuleShape3D = CapsuleShape3D.new()
193
+ s.radius = CommandParams.json_float(shape_params, "radius", 0.5)
194
+ s.height = CommandParams.json_float(shape_params, "height", 2.0)
195
+ return s
196
+ "cylinder":
197
+ var s: CylinderShape3D = CylinderShape3D.new()
198
+ s.radius = CommandParams.json_float(shape_params, "radius", 0.5)
199
+ s.height = CommandParams.json_float(shape_params, "height", 2.0)
200
+ return s
201
+ "ray":
202
+ var s: SeparationRayShape3D = SeparationRayShape3D.new()
203
+ s.length = CommandParams.json_float(shape_params, "length", 1.0)
204
+ return s
205
+ return null
206
+
207
+
208
+ func _shape_2d(shape_type: String, shape_params: Dictionary) -> Shape2D:
209
+ match shape_type:
210
+ "box":
211
+ var s: RectangleShape2D = RectangleShape2D.new()
212
+ s.size = Vector2(CommandParams.json_float(shape_params, "size_x", 1), CommandParams.json_float(shape_params, "size_y", 1))
213
+ return s
214
+ "circle":
215
+ var s: CircleShape2D = CircleShape2D.new()
216
+ s.radius = CommandParams.json_float(shape_params, "radius", 0.5)
217
+ return s
218
+ "capsule":
219
+ var s: CapsuleShape2D = CapsuleShape2D.new()
220
+ s.radius = CommandParams.json_float(shape_params, "radius", 0.5)
221
+ s.height = CommandParams.json_float(shape_params, "height", 2.0)
222
+ return s
223
+ "segment":
224
+ var s: SegmentShape2D = SegmentShape2D.new()
225
+ s.a = Vector2(CommandParams.json_float(shape_params, "a_x", 0), CommandParams.json_float(shape_params, "a_y", 0))
226
+ s.b = Vector2(CommandParams.json_float(shape_params, "b_x", 1), CommandParams.json_float(shape_params, "b_y", 0))
227
+ return s
228
+ return null
229
+
230
+
231
+ # --- Physics body properties ---
232
+ func _cmd_physics_body(params: Dictionary) -> void:
233
+ var reader: CommandParams = CommandParams.new(params)
234
+ var node: Node = require_node(reader)
235
+ if node != null and not (node is PhysicsBody2D or node is PhysicsBody3D):
236
+ _require_class(reader, node, "PhysicsBody2D or PhysicsBody3D")
237
+ var gravity_scale: float = reader.optional_number("gravity_scale", 0.0)
238
+ var mass: float = reader.optional_number("mass", 0.0)
239
+ var freeze: bool = reader.optional_bool("freeze", false)
240
+ var sleeping: bool = reader.optional_bool("sleeping", false)
241
+ var linear_damp: float = reader.optional_number("linear_damp", 0.0)
242
+ var angular_damp: float = reader.optional_number("angular_damp", 0.0)
243
+ var linear_velocity: Dictionary = reader.optional_dictionary("linear_velocity")
244
+ var angular_velocity: Variant = reader.raw("angular_velocity")
245
+ if reader.has_param("angular_velocity") and not (angular_velocity is Dictionary or angular_velocity is float or angular_velocity is int):
246
+ reader.fail("angular_velocity must be a number or an object", {"param": "angular_velocity", "reason": "invalid_type"})
247
+ var friction: float = reader.optional_number("friction", 0.0)
248
+ var bounce: float = reader.optional_number("bounce", 0.0)
249
+ if params_invalid(reader):
250
+ return
251
+
252
+ # Common properties are set only where the concrete body class exposes them.
253
+ if reader.has_param("gravity_scale") and node.get("gravity_scale") != null:
254
+ node.set("gravity_scale", gravity_scale)
255
+ if reader.has_param("mass") and node.get("mass") != null:
256
+ node.set("mass", mass)
257
+ if reader.has_param("freeze") and node.get("freeze") != null:
258
+ node.set("freeze", freeze)
259
+ if reader.has_param("sleeping") and node.get("sleeping") != null:
260
+ node.set("sleeping", sleeping)
261
+ if reader.has_param("linear_damp") and node.get("linear_damp") != null:
262
+ node.set("linear_damp", linear_damp)
263
+ if reader.has_param("angular_damp") and node.get("angular_damp") != null:
264
+ node.set("angular_damp", angular_damp)
265
+
266
+ # Velocity (2D vs 3D)
267
+ if reader.has_param("linear_velocity"):
268
+ if node is PhysicsBody3D:
269
+ node.set("linear_velocity", _vector3_from(linear_velocity))
270
+ else:
271
+ node.set("linear_velocity", _vector2_from(linear_velocity))
272
+ if reader.has_param("angular_velocity"):
273
+ if node is PhysicsBody3D and angular_velocity is Dictionary:
274
+ node.set("angular_velocity", _vector3_from(angular_velocity))
275
+ else:
276
+ node.set("angular_velocity", CommandParams.to_float(angular_velocity))
277
+
278
+ # Physics material (friction, bounce)
279
+ if reader.has_param("friction") or reader.has_param("bounce"):
280
+ var material_value: Variant = node.get("physics_material_override")
281
+ var phys_mat: PhysicsMaterial = null
282
+ if material_value is PhysicsMaterial:
283
+ phys_mat = material_value
284
+ if phys_mat == null:
285
+ phys_mat = PhysicsMaterial.new()
286
+ node.set("physics_material_override", phys_mat)
287
+ if reader.has_param("friction"):
288
+ phys_mat.friction = friction
289
+ if reader.has_param("bounce"):
290
+ phys_mat.bounce = bounce
291
+
292
+ var result: Dictionary = {"success": true, "node_path": params.get("node_path"), "class": node.get_class()}
293
+ if node.get("mass") != null:
294
+ result["mass"] = node.get("mass")
295
+ if node.get("gravity_scale") != null:
296
+ result["gravity_scale"] = node.get("gravity_scale")
297
+ if node.get("linear_velocity") != null:
298
+ result["linear_velocity"] = variant_to_json(node.get("linear_velocity"))
299
+ if node.get("angular_velocity") != null:
300
+ result["angular_velocity"] = variant_to_json(node.get("angular_velocity"))
301
+ respond(result)
302
+
303
+
304
+ # --- Joints ---
305
+ func _cmd_create_joint(params: Dictionary) -> void:
306
+ var reader: CommandParams = CommandParams.new(params)
307
+ var parent: Node = require_node(reader, "parent_path")
308
+ var joint_type: String = reader.required_enum("joint_type", JOINT_TYPES)
309
+ var node_a: String = reader.optional_string("node_a_path")
310
+ var node_b: String = reader.optional_string("node_b_path")
311
+ var softness: float = reader.optional_number("softness", 0.0)
312
+ var length: float = reader.optional_number("length", 0.0)
313
+ var rest_length: float = reader.optional_number("rest_length", 0.0)
314
+ var stiffness: float = reader.optional_number("stiffness", 0.0)
315
+ var damping: float = reader.optional_number("damping", 0.0)
316
+ var initial_offset: float = reader.optional_number("initial_offset", 0.0)
317
+ if params_invalid(reader):
318
+ return
319
+
320
+ var joint: Node = null
321
+ match joint_type:
322
+ "pin_2d":
323
+ var j: PinJoint2D = PinJoint2D.new()
324
+ if reader.has_param("softness"):
325
+ j.softness = softness
326
+ joint = j
327
+ "spring_2d":
328
+ var j: DampedSpringJoint2D = DampedSpringJoint2D.new()
329
+ if reader.has_param("length"):
330
+ j.length = length
331
+ if reader.has_param("rest_length"):
332
+ j.rest_length = rest_length
333
+ if reader.has_param("stiffness"):
334
+ j.stiffness = stiffness
335
+ if reader.has_param("damping"):
336
+ j.damping = damping
337
+ joint = j
338
+ "groove_2d":
339
+ var j: GrooveJoint2D = GrooveJoint2D.new()
340
+ if reader.has_param("length"):
341
+ j.length = length
342
+ if reader.has_param("initial_offset"):
343
+ j.initial_offset = initial_offset
344
+ joint = j
345
+ "pin_3d":
346
+ joint = PinJoint3D.new()
347
+ "hinge_3d":
348
+ joint = HingeJoint3D.new()
349
+ "cone_3d":
350
+ joint = ConeTwistJoint3D.new()
351
+ "slider_3d":
352
+ joint = SliderJoint3D.new()
353
+ # Joint2D and Joint3D both expose node_a/node_b as NodePath properties.
354
+ if not node_a.is_empty():
355
+ joint.set("node_a", NodePath(node_a))
356
+ if not node_b.is_empty():
357
+ joint.set("node_b", NodePath(node_b))
358
+ parent.add_child(joint)
359
+ var expects_3d: bool = joint is Joint3D
360
+ for endpoint: String in [node_a, node_b]:
361
+ if endpoint.is_empty():
362
+ continue
363
+ var body: Node = joint.get_node_or_null(NodePath(endpoint))
364
+ var valid_body: bool = body is PhysicsBody3D if expects_3d else body is PhysicsBody2D
365
+ if not valid_body:
366
+ joint.queue_free()
367
+ respond({"error": "Joint endpoint is not a matching physics body: %s" % endpoint})
368
+ return
369
+ respond({"success": true, "joint_type": joint_type, "name": joint.name, "path": str(joint.get_path())})
370
+
371
+
372
+ # --- NavigationRegion3D management ---
373
+ func _cmd_navigation_3d(params: Dictionary) -> void:
374
+ var reader: CommandParams = CommandParams.new(params)
375
+ var action: String = reader.optional_enum("action", "create", ["create", "bake"])
376
+ if params_invalid(reader):
377
+ return
378
+
379
+ match action:
380
+ "create":
381
+ var parent: Node = require_node(reader, "parent_path", "/root")
382
+ var cell_size: float = reader.optional_number("cell_size", 0.0)
383
+ var agent_radius: float = reader.optional_number("agent_radius", 0.0)
384
+ var agent_height: float = reader.optional_number("agent_height", 0.0)
385
+ if params_invalid(reader):
386
+ return
387
+ var region: NavigationRegion3D = NavigationRegion3D.new()
388
+ region.navigation_mesh = NavigationMesh.new()
389
+ if reader.has_param("cell_size"):
390
+ region.navigation_mesh.cell_size = cell_size
391
+ if reader.has_param("agent_radius"):
392
+ region.navigation_mesh.agent_radius = agent_radius
393
+ if reader.has_param("agent_height"):
394
+ region.navigation_mesh.agent_height = agent_height
395
+ _apply_optional_name(reader, region)
396
+ parent.add_child(region)
397
+ var map_rid: RID = get_viewport().world_3d.get_navigation_map()
398
+ NavigationServer3D.map_set_active(map_rid, true)
399
+ if reader.has_param("cell_size"):
400
+ NavigationServer3D.map_set_cell_size(map_rid, cell_size)
401
+ region.set_navigation_map(map_rid)
402
+ NavigationServer3D.region_set_map(region.get_rid(), map_rid)
403
+ respond({"success": true, "action": "create", "path": str(region.get_path())})
404
+ "bake":
405
+ var node: Node = require_node(reader)
406
+ if node != null and not node is NavigationRegion3D:
407
+ _require_class(reader, node, "NavigationRegion3D")
408
+ if params_invalid(reader):
409
+ return
410
+ var region: NavigationRegion3D = node
411
+ region.bake_navigation_mesh(false)
412
+ NavigationServer3D.region_set_navigation_mesh(region.get_rid(), region.navigation_mesh)
413
+ var map_rid: RID = region.get_navigation_map()
414
+ NavigationServer3D.region_set_map(region.get_rid(), map_rid)
415
+ NavigationServer3D.map_force_update(map_rid)
416
+ respond({"success": true, "action": "bake"})
417
+
418
+
419
+ # --- 3D direct-space queries ---
420
+ func _cmd_physics_3d(params: Dictionary) -> void:
421
+ var reader: CommandParams = CommandParams.new(params)
422
+ var action: String = reader.optional_enum("action", "ray", ["ray", "overlap", "contacts", "inspect_shape"])
423
+ if params_invalid(reader):
424
+ return
425
+
426
+ match action:
427
+ "ray":
428
+ var from_dict: Dictionary = reader.required_dictionary("from")
429
+ var to_dict: Dictionary = reader.required_dictionary("to")
430
+ var collision_mask: int = reader.optional_int("collision_mask", 0)
431
+ if params_invalid(reader):
432
+ return
433
+ await get_tree().physics_frame
434
+ var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(_vector3_from(from_dict), _vector3_from(to_dict))
435
+ if reader.has_param("collision_mask"):
436
+ query.collision_mask = collision_mask
437
+ var result: Dictionary = get_viewport().world_3d.direct_space_state.intersect_ray(query)
438
+ if result.is_empty():
439
+ respond({"success": true, "action": "ray", "hit": false})
440
+ else:
441
+ respond({"success": true, "action": "ray", "hit": true, "position": variant_to_json(result["position"]), "normal": variant_to_json(result["normal"]), "collider": str(result.get("collider", ""))})
442
+ "overlap":
443
+ var node: Node = require_node(reader)
444
+ if node != null and not node is Area3D:
445
+ _require_class(reader, node, "Area3D")
446
+ if params_invalid(reader):
447
+ return
448
+ # `physics_frame` is emitted before the physics step. Wait for the
449
+ # following process frame so the direct space state is current.
450
+ await get_tree().physics_frame
451
+ await get_tree().process_frame
452
+ var out: Array = _query_area_3d_bodies(node as Area3D)
453
+ respond({"success": true, "action": "overlap", "bodies": out})
454
+ "contacts":
455
+ var body: Node = require_node(reader)
456
+ if body != null and not body.has_method("get_contact_count"):
457
+ reader.fail("node must expose get_contact_count", {"param": "node_path", "reason": "not_physics_body"})
458
+ if params_invalid(reader):
459
+ return
460
+ await get_tree().physics_frame
461
+ var contacts: Array = []
462
+ var count_value: Variant = body.call("get_contact_count")
463
+ var count: int = count_value if count_value is int else 0
464
+ for index: int in range(min(count, 256)):
465
+ contacts.append({
466
+ "collider": str(body.call("get_contact_collider_object", index)),
467
+ "position": variant_to_json(body.call("get_contact_collider_position", index)),
468
+ "normal": variant_to_json(body.call("get_contact_local_normal", index)),
469
+ })
470
+ respond({"success": true, "action": "contacts", "count": count, "contacts": contacts, "truncated": count > contacts.size()})
471
+ "inspect_shape":
472
+ var shape_node: Node = require_node(reader)
473
+ if shape_node != null and not shape_node is CollisionShape3D:
474
+ _require_class(reader, shape_node, "CollisionShape3D")
475
+ if params_invalid(reader):
476
+ return
477
+ var shape: Shape3D = (shape_node as CollisionShape3D).shape
478
+ if shape == null:
479
+ respond({"success": true, "action": "inspect_shape", "shape": null})
480
+ return
481
+ respond({"success": true, "action": "inspect_shape", "shape": {
482
+ "type": shape.get_class(),
483
+ "resource_path": shape.resource_path,
484
+ "resource_local_to_scene": shape.resource_local_to_scene,
485
+ "size": variant_to_json(shape.get("size")),
486
+ "radius": shape.get("radius"),
487
+ "height": shape.get("height"),
488
+ }})
489
+
490
+
491
+ # --- 2D direct-space queries ---
492
+ func _cmd_physics_2d(params: Dictionary) -> void:
493
+ var reader: CommandParams = CommandParams.new(params)
494
+ var action: String = reader.optional_enum("action", "ray", ["ray", "overlap", "point_query", "shape_query"])
495
+ if params_invalid(reader):
496
+ return
497
+
498
+ match action:
499
+ "ray":
500
+ var from_dict: Dictionary = reader.optional_dictionary("from")
501
+ var to_dict: Dictionary = reader.optional_dictionary("to")
502
+ var collision_mask: int = reader.optional_int("collision_mask", 0)
503
+ if params_invalid(reader):
504
+ return
505
+ await get_tree().physics_frame
506
+ var query: PhysicsRayQueryParameters2D = PhysicsRayQueryParameters2D.create(_vector2_from(from_dict), _vector2_from(to_dict))
507
+ if reader.has_param("collision_mask"):
508
+ query.collision_mask = collision_mask
509
+ var result: Dictionary = get_viewport().world_2d.direct_space_state.intersect_ray(query)
510
+ if result.is_empty():
511
+ respond({"success": true, "action": "ray", "hit": false})
512
+ else:
513
+ respond({"success": true, "action": "ray", "hit": true, "position": variant_to_json(result["position"]), "normal": variant_to_json(result["normal"]), "collider": str(result.get("collider", ""))})
514
+ "overlap":
515
+ var node: Node = require_node(reader)
516
+ if node != null and not node is Area2D:
517
+ _require_class(reader, node, "Area2D")
518
+ if params_invalid(reader):
519
+ return
520
+ # Complete a physics step before reading the direct space state.
521
+ await get_tree().physics_frame
522
+ await get_tree().process_frame
523
+ var out: Array = _query_area_2d_bodies(node as Area2D)
524
+ respond({"success": true, "action": "overlap", "bodies": out})
525
+ "point_query":
526
+ var position: Dictionary = reader.optional_dictionary("position", reader.optional_dictionary("point"))
527
+ if not reader.failed() and position.is_empty():
528
+ reader.fail("position must be a position object", {"param": "position", "reason": "missing"})
529
+ var collide_with_areas: bool = reader.optional_bool("collide_with_areas", true)
530
+ var collide_with_bodies: bool = reader.optional_bool("collide_with_bodies", true)
531
+ var collision_mask: int = reader.optional_int("collision_mask", 0)
532
+ var max_results: int = reader.optional_int("max_results", 32, 1)
533
+ if params_invalid(reader):
534
+ return
535
+ await get_tree().physics_frame
536
+ var query: PhysicsPointQueryParameters2D = PhysicsPointQueryParameters2D.new()
537
+ query.position = _vector2_from(position)
538
+ query.collide_with_areas = collide_with_areas
539
+ query.collide_with_bodies = collide_with_bodies
540
+ if reader.has_param("collision_mask"):
541
+ query.collision_mask = collision_mask
542
+ var hits: Array = get_viewport().world_2d.direct_space_state.intersect_point(query, max_results)
543
+ var out: Array = []
544
+ for hit: Dictionary in hits:
545
+ out.append({"collider": str(hit.get("collider", "")), "rid": str(hit.get("rid", ""))})
546
+ respond({"success": true, "action": "point_query", "count": out.size(), "results": out})
547
+ "shape_query":
548
+ var shape_type: String = reader.optional_enum("shape_type", "circle", ["circle", "rectangle"])
549
+ var size: Dictionary = reader.optional_dictionary("size", {"x": 10, "y": 10})
550
+ var radius: float = reader.optional_number("radius", 10.0)
551
+ var position: Dictionary = reader.optional_dictionary("position")
552
+ var collide_with_areas: bool = reader.optional_bool("collide_with_areas", true)
553
+ var collide_with_bodies: bool = reader.optional_bool("collide_with_bodies", true)
554
+ var collision_mask: int = reader.optional_int("collision_mask", 0)
555
+ var max_results: int = reader.optional_int("max_results", 32, 1)
556
+ if params_invalid(reader):
557
+ return
558
+ await get_tree().physics_frame
559
+ var shape: Shape2D
560
+ if shape_type == "rectangle":
561
+ var rect: RectangleShape2D = RectangleShape2D.new()
562
+ rect.size = Vector2(CommandParams.json_float(size, "x", 10), CommandParams.json_float(size, "y", 10))
563
+ shape = rect
564
+ else:
565
+ var circle: CircleShape2D = CircleShape2D.new()
566
+ circle.radius = radius
567
+ shape = circle
568
+ var query: PhysicsShapeQueryParameters2D = PhysicsShapeQueryParameters2D.new()
569
+ query.shape = shape
570
+ var transform: Transform2D = Transform2D.IDENTITY
571
+ transform.origin = _vector2_from(position)
572
+ query.transform = transform
573
+ query.collide_with_areas = collide_with_areas
574
+ query.collide_with_bodies = collide_with_bodies
575
+ if reader.has_param("collision_mask"):
576
+ query.collision_mask = collision_mask
577
+ var hits: Array = get_viewport().world_2d.direct_space_state.intersect_shape(query, max_results)
578
+ var out: Array = []
579
+ for hit: Dictionary in hits:
580
+ out.append({"collider": str(hit.get("collider", "")), "rid": str(hit.get("rid", ""))})
581
+ respond({"success": true, "action": "shape_query", "count": out.size(), "results": out})
582
+
583
+
584
+ # Area overlap caches can lag behind the command loop depending on frame
585
+ # scheduling. Direct-space queries provide deterministic current-state reads.
586
+ func _query_area_3d_bodies(area: Area3D) -> Array:
587
+ var out: Array = []
588
+ if not area.monitoring:
589
+ return out
590
+ var seen: Dictionary = {}
591
+ var space_state: PhysicsDirectSpaceState3D = area.get_world_3d().direct_space_state
592
+ for child: Node in area.get_children():
593
+ if not child is CollisionShape3D:
594
+ continue
595
+ var collision_shape: CollisionShape3D = child as CollisionShape3D
596
+ if collision_shape.disabled or collision_shape.shape == null:
597
+ continue
598
+ var query := PhysicsShapeQueryParameters3D.new()
599
+ query.shape = collision_shape.shape
600
+ query.transform = collision_shape.global_transform
601
+ query.collision_mask = area.collision_mask
602
+ query.exclude = [area.get_rid()]
603
+ query.collide_with_areas = false
604
+ query.collide_with_bodies = true
605
+ for hit: Dictionary in space_state.intersect_shape(query, 256):
606
+ var collider: Variant = hit.get("collider")
607
+ if not collider is PhysicsBody3D:
608
+ continue
609
+ @warning_ignore("unsafe_cast")
610
+ var body: PhysicsBody3D = collider as PhysicsBody3D
611
+ if body.collision_mask & area.collision_layer == 0:
612
+ continue
613
+ var id: int = body.get_instance_id()
614
+ if seen.has(id):
615
+ continue
616
+ seen[id] = true
617
+ out.append({"name": body.name, "path": str(body.get_path())})
618
+ return out
619
+
620
+
621
+ func _query_area_2d_bodies(area: Area2D) -> Array:
622
+ var out: Array = []
623
+ if not area.monitoring:
624
+ return out
625
+ var seen: Dictionary = {}
626
+ var space_state: PhysicsDirectSpaceState2D = area.get_world_2d().direct_space_state
627
+ for child: Node in area.get_children():
628
+ if not child is CollisionShape2D:
629
+ continue
630
+ var collision_shape: CollisionShape2D = child as CollisionShape2D
631
+ if collision_shape.disabled or collision_shape.shape == null:
632
+ continue
633
+ var query := PhysicsShapeQueryParameters2D.new()
634
+ query.shape = collision_shape.shape
635
+ query.transform = collision_shape.global_transform
636
+ query.collision_mask = area.collision_mask
637
+ query.exclude = [area.get_rid()]
638
+ query.collide_with_areas = false
639
+ query.collide_with_bodies = true
640
+ for hit: Dictionary in space_state.intersect_shape(query, 256):
641
+ var collider: Variant = hit.get("collider")
642
+ if not collider is PhysicsBody2D:
643
+ continue
644
+ @warning_ignore("unsafe_cast")
645
+ var body: PhysicsBody2D = collider as PhysicsBody2D
646
+ if body.collision_mask & area.collision_layer == 0:
647
+ continue
648
+ var id: int = body.get_instance_id()
649
+ if seen.has(id):
650
+ continue
651
+ seen[id] = true
652
+ out.append({"name": body.name, "path": str(body.get_path())})
653
+ return out