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