@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,568 @@
|
|
|
1
|
+
extends "res://mcp_runtime/runtime_domain.gd"
|
|
2
|
+
|
|
3
|
+
# Audio and animation runtime commands.
|
|
4
|
+
|
|
5
|
+
func register_commands() -> void:
|
|
6
|
+
register_command("get_audio", _cmd_get_audio)
|
|
7
|
+
register_command("audio_play", _cmd_audio_play)
|
|
8
|
+
register_command("audio_bus", _cmd_audio_bus)
|
|
9
|
+
register_command("audio_effect", _cmd_audio_effect)
|
|
10
|
+
register_command("audio_bus_layout", _cmd_audio_bus_layout)
|
|
11
|
+
register_command("audio_spatial", _cmd_audio_spatial)
|
|
12
|
+
register_command("create_animation", _cmd_create_animation)
|
|
13
|
+
register_command("animation_tree", _cmd_animation_tree)
|
|
14
|
+
register_command("animation_control", _cmd_animation_control)
|
|
15
|
+
register_command("skeleton_ik", _cmd_skeleton_ik)
|
|
16
|
+
register_command("bone_pose", _cmd_bone_pose)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
func _cmd_get_audio(_params: Dictionary) -> void:
|
|
20
|
+
var buses: Array = []
|
|
21
|
+
for i in AudioServer.bus_count:
|
|
22
|
+
buses.append({
|
|
23
|
+
"name": AudioServer.get_bus_name(i),
|
|
24
|
+
"volume_db": AudioServer.get_bus_volume_db(i),
|
|
25
|
+
"mute": AudioServer.is_bus_mute(i),
|
|
26
|
+
"solo": AudioServer.is_bus_solo(i),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
var players: Array = []
|
|
30
|
+
_find_audio_players(get_tree().root, players)
|
|
31
|
+
|
|
32
|
+
respond({"success": true, "buses": buses, "players": players})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
func _find_audio_players(node: Node, results: Array) -> void:
|
|
36
|
+
if node is AudioStreamPlayer:
|
|
37
|
+
var p: AudioStreamPlayer = node as AudioStreamPlayer
|
|
38
|
+
results.append({"path": str(p.get_path()), "type": "AudioStreamPlayer", "playing": p.playing, "bus": p.bus})
|
|
39
|
+
elif node is AudioStreamPlayer2D:
|
|
40
|
+
var p: AudioStreamPlayer2D = node as AudioStreamPlayer2D
|
|
41
|
+
results.append({"path": str(p.get_path()), "type": "AudioStreamPlayer2D", "playing": p.playing, "bus": p.bus})
|
|
42
|
+
elif node is AudioStreamPlayer3D:
|
|
43
|
+
var p: AudioStreamPlayer3D = node as AudioStreamPlayer3D
|
|
44
|
+
results.append({"path": str(p.get_path()), "type": "AudioStreamPlayer3D", "playing": p.playing, "bus": p.bus})
|
|
45
|
+
for child in node.get_children():
|
|
46
|
+
_find_audio_players(child, results)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# --- Spawn Node ---
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
func _cmd_audio_play(params: Dictionary) -> void:
|
|
53
|
+
var reader := CommandParams.new(params)
|
|
54
|
+
var node: Node = require_node(reader)
|
|
55
|
+
var action: String = reader.optional_enum("action", "play", ["play", "stop", "pause", "resume"])
|
|
56
|
+
if params_invalid(reader):
|
|
57
|
+
return
|
|
58
|
+
var node_path: String = str(node.get_path())
|
|
59
|
+
if not (node is AudioStreamPlayer or node is AudioStreamPlayer2D or node is AudioStreamPlayer3D):
|
|
60
|
+
reader.fail("node_path must reference an AudioStreamPlayer", {"param": "node_path", "reason": "invalid_value", "value": node_path, "class": node.get_class()})
|
|
61
|
+
send_params_error(reader)
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# Optionally load a new stream
|
|
65
|
+
if params.has("stream"):
|
|
66
|
+
var stream_path: String = params["stream"]
|
|
67
|
+
var stream: AudioStream = load(stream_path) as AudioStream
|
|
68
|
+
if stream == null:
|
|
69
|
+
respond({"error": "Failed to load audio stream: %s" % stream_path})
|
|
70
|
+
return
|
|
71
|
+
node.set("stream", stream)
|
|
72
|
+
|
|
73
|
+
# Set optional properties
|
|
74
|
+
if params.has("volume"):
|
|
75
|
+
var linear_vol: float = CommandParams.to_float(params["volume"])
|
|
76
|
+
node.set("volume_db", linear_to_db(clampf(linear_vol, 0.0, 1.0)))
|
|
77
|
+
if params.has("pitch"):
|
|
78
|
+
node.set("pitch_scale", CommandParams.to_float(params["pitch"]))
|
|
79
|
+
if params.has("bus"):
|
|
80
|
+
node.set("bus", params["bus"])
|
|
81
|
+
|
|
82
|
+
match action:
|
|
83
|
+
"play":
|
|
84
|
+
var from_pos: float = CommandParams.json_float(params, "from_position", 0.0)
|
|
85
|
+
node.call("play", from_pos)
|
|
86
|
+
respond({"success": true, "action": "play", "node_path": node_path})
|
|
87
|
+
"stop":
|
|
88
|
+
node.call("stop")
|
|
89
|
+
respond({"success": true, "action": "stop", "node_path": node_path})
|
|
90
|
+
"pause":
|
|
91
|
+
node.set("stream_paused", true)
|
|
92
|
+
respond({"success": true, "action": "pause", "node_path": node_path})
|
|
93
|
+
"resume":
|
|
94
|
+
node.set("stream_paused", false)
|
|
95
|
+
respond({"success": true, "action": "resume", "node_path": node_path})
|
|
96
|
+
_:
|
|
97
|
+
respond({"error": "Unknown audio action: %s. Use play, stop, pause, or resume" % action})
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --- Audio Bus ---
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
func _cmd_audio_bus(params: Dictionary) -> void:
|
|
104
|
+
var bus_name: String = params.get("bus_name", "Master")
|
|
105
|
+
var bus_idx: int = AudioServer.get_bus_index(bus_name)
|
|
106
|
+
if bus_idx == -1:
|
|
107
|
+
respond({"error": "Audio bus not found: %s" % bus_name})
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
if params.has("volume"):
|
|
111
|
+
var linear_vol: float = CommandParams.to_float(params["volume"])
|
|
112
|
+
AudioServer.set_bus_volume_db(bus_idx, linear_to_db(clampf(linear_vol, 0.0, 1.0)))
|
|
113
|
+
if params.has("mute"):
|
|
114
|
+
AudioServer.set_bus_mute(bus_idx, CommandParams.to_bool(params["mute"]))
|
|
115
|
+
if params.has("solo"):
|
|
116
|
+
AudioServer.set_bus_solo(bus_idx, CommandParams.to_bool(params["solo"]))
|
|
117
|
+
|
|
118
|
+
respond({
|
|
119
|
+
"success": true,
|
|
120
|
+
"bus_name": bus_name,
|
|
121
|
+
"volume_db": AudioServer.get_bus_volume_db(bus_idx),
|
|
122
|
+
"mute": AudioServer.is_bus_mute(bus_idx),
|
|
123
|
+
"solo": AudioServer.is_bus_solo(bus_idx)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# --- Environment / Post-Processing ---
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
func _cmd_create_animation(params: Dictionary) -> void:
|
|
131
|
+
var node_path: String = params.get("node_path", "")
|
|
132
|
+
var anim_name: String = params.get("animation_name", "")
|
|
133
|
+
if node_path.is_empty() or anim_name.is_empty():
|
|
134
|
+
respond({"error": "node_path and animation_name are required"})
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
var node: Node = get_tree().root.get_node_or_null(node_path)
|
|
138
|
+
if node == null:
|
|
139
|
+
respond({"error": "Node not found: %s" % node_path})
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
if not node is AnimationPlayer:
|
|
143
|
+
respond({"error": "Node is not an AnimationPlayer: %s (is %s)" % [node_path, node.get_class()]})
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
var anim_player: AnimationPlayer = node as AnimationPlayer
|
|
147
|
+
var anim: Animation = Animation.new()
|
|
148
|
+
anim.length = CommandParams.json_float(params, "length", 1.0)
|
|
149
|
+
var loop_mode: int = CommandParams.json_int(params, "loop_mode", 0)
|
|
150
|
+
anim.loop_mode = loop_mode as Animation.LoopMode
|
|
151
|
+
|
|
152
|
+
var tracks: Array = CommandParams.json_array(params, "tracks")
|
|
153
|
+
var track_count: int = 0
|
|
154
|
+
for track_entry: Variant in tracks:
|
|
155
|
+
var track_data: Dictionary = CommandParams.as_dictionary(track_entry)
|
|
156
|
+
var track_type_str: String = CommandParams.json_string(track_data, "type", "value")
|
|
157
|
+
var track_path: String = CommandParams.json_string(track_data, "path")
|
|
158
|
+
if track_path.is_empty():
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
var track_type: int = Animation.TYPE_VALUE
|
|
162
|
+
match track_type_str:
|
|
163
|
+
"value":
|
|
164
|
+
track_type = Animation.TYPE_VALUE
|
|
165
|
+
"method":
|
|
166
|
+
track_type = Animation.TYPE_METHOD
|
|
167
|
+
"bezier":
|
|
168
|
+
track_type = Animation.TYPE_BEZIER
|
|
169
|
+
"audio":
|
|
170
|
+
track_type = Animation.TYPE_AUDIO
|
|
171
|
+
|
|
172
|
+
var idx: int = anim.add_track(track_type)
|
|
173
|
+
anim.track_set_path(idx, NodePath(track_path))
|
|
174
|
+
|
|
175
|
+
var keys: Array = CommandParams.json_array(track_data, "keys")
|
|
176
|
+
for key_entry: Variant in keys:
|
|
177
|
+
var key_data: Dictionary = CommandParams.as_dictionary(key_entry)
|
|
178
|
+
var time: float = CommandParams.json_float(key_data, "time", 0.0)
|
|
179
|
+
match track_type:
|
|
180
|
+
Animation.TYPE_VALUE:
|
|
181
|
+
var value: Variant = json_to_variant(key_data.get("value"), CommandParams.json_string(key_data, "type_hint"))
|
|
182
|
+
@warning_ignore("return_value_discarded")
|
|
183
|
+
anim.track_insert_key(idx, time, value)
|
|
184
|
+
if key_data.has("transition"):
|
|
185
|
+
var key_idx: int = anim.track_find_key(idx, time, Animation.FIND_MODE_APPROX)
|
|
186
|
+
if key_idx >= 0:
|
|
187
|
+
anim.track_set_key_transition(idx, key_idx, CommandParams.to_float(key_data["transition"]))
|
|
188
|
+
Animation.TYPE_METHOD:
|
|
189
|
+
var method_name: String = CommandParams.json_string(key_data, "method")
|
|
190
|
+
var args: Array = CommandParams.json_array(key_data, "args")
|
|
191
|
+
@warning_ignore("return_value_discarded")
|
|
192
|
+
anim.track_insert_key(idx, time, {"method": method_name, "args": args})
|
|
193
|
+
Animation.TYPE_BEZIER:
|
|
194
|
+
var value: float = CommandParams.json_float(key_data, "value", 0.0)
|
|
195
|
+
@warning_ignore("return_value_discarded")
|
|
196
|
+
anim.bezier_track_insert_key(idx, time, value)
|
|
197
|
+
Animation.TYPE_AUDIO:
|
|
198
|
+
var stream_path: String = CommandParams.json_string(key_data, "stream")
|
|
199
|
+
if not stream_path.is_empty():
|
|
200
|
+
var stream: AudioStream = load(stream_path) as AudioStream
|
|
201
|
+
if stream != null:
|
|
202
|
+
@warning_ignore("return_value_discarded")
|
|
203
|
+
anim.audio_track_insert_key(idx, time, stream)
|
|
204
|
+
track_count += 1
|
|
205
|
+
|
|
206
|
+
# Add to library (use default "" library if it exists, otherwise create it)
|
|
207
|
+
var lib_name: String = CommandParams.json_string(params, "library")
|
|
208
|
+
var lib: AnimationLibrary = null
|
|
209
|
+
if anim_player.has_animation_library(lib_name):
|
|
210
|
+
lib = anim_player.get_animation_library(lib_name)
|
|
211
|
+
else:
|
|
212
|
+
lib = AnimationLibrary.new()
|
|
213
|
+
@warning_ignore("return_value_discarded")
|
|
214
|
+
anim_player.add_animation_library(lib_name, lib)
|
|
215
|
+
if lib.has_animation(anim_name):
|
|
216
|
+
respond({"error": "Animation already exists: %s" % anim_name})
|
|
217
|
+
return
|
|
218
|
+
@warning_ignore("return_value_discarded")
|
|
219
|
+
lib.add_animation(anim_name, anim)
|
|
220
|
+
|
|
221
|
+
respond({"success": true, "animation_name": anim_name, "length": anim.length, "loop_mode": loop_mode, "track_count": track_count})
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# --- Serialize State ---
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
func _cmd_bone_pose(params: Dictionary) -> void:
|
|
228
|
+
var reader := CommandParams.new(params)
|
|
229
|
+
var node: Node = require_node(reader)
|
|
230
|
+
var action: String = reader.optional_enum("action", "list", ["list", "get", "set"])
|
|
231
|
+
if params_invalid(reader):
|
|
232
|
+
return
|
|
233
|
+
var node_path: String = str(node.get_path())
|
|
234
|
+
if not node is Skeleton3D:
|
|
235
|
+
reader.fail("node_path must reference a Skeleton3D", {"param": "node_path", "reason": "invalid_value", "value": node_path, "class": node.get_class()})
|
|
236
|
+
send_params_error(reader)
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
var skel: Skeleton3D = node as Skeleton3D
|
|
240
|
+
|
|
241
|
+
match action:
|
|
242
|
+
"list":
|
|
243
|
+
var bones: Array = []
|
|
244
|
+
for i in skel.get_bone_count():
|
|
245
|
+
bones.append({"index": i, "name": skel.get_bone_name(i), "parent": skel.get_bone_parent(i)})
|
|
246
|
+
respond({"success": true, "action": "list", "bone_count": skel.get_bone_count(), "bones": bones})
|
|
247
|
+
"get":
|
|
248
|
+
var bone_idx: int = _resolve_bone_index(skel, params)
|
|
249
|
+
if bone_idx < 0:
|
|
250
|
+
respond({"error": "Bone not found"})
|
|
251
|
+
return
|
|
252
|
+
respond({
|
|
253
|
+
"success": true, "action": "get", "bone_index": bone_idx,
|
|
254
|
+
"bone_name": skel.get_bone_name(bone_idx),
|
|
255
|
+
"position": variant_to_json(skel.get_bone_pose_position(bone_idx)),
|
|
256
|
+
"rotation": variant_to_json(skel.get_bone_pose_rotation(bone_idx)),
|
|
257
|
+
"scale": variant_to_json(skel.get_bone_pose_scale(bone_idx))
|
|
258
|
+
})
|
|
259
|
+
"set":
|
|
260
|
+
var bone_idx: int = _resolve_bone_index(skel, params)
|
|
261
|
+
if bone_idx < 0:
|
|
262
|
+
respond({"error": "Bone not found"})
|
|
263
|
+
return
|
|
264
|
+
if params.has("position"):
|
|
265
|
+
var p: Dictionary = params["position"]
|
|
266
|
+
skel.set_bone_pose_position(bone_idx, Vector3(CommandParams.json_float(p, "x", 0), CommandParams.json_float(p, "y", 0), CommandParams.json_float(p, "z", 0)))
|
|
267
|
+
if params.has("rotation"):
|
|
268
|
+
var r: Dictionary = params["rotation"]
|
|
269
|
+
skel.set_bone_pose_rotation(bone_idx, Quaternion(CommandParams.json_float(r, "x", 0), CommandParams.json_float(r, "y", 0), CommandParams.json_float(r, "z", 0), CommandParams.json_float(r, "w", 1)))
|
|
270
|
+
if params.has("scale"):
|
|
271
|
+
var s: Dictionary = params["scale"]
|
|
272
|
+
skel.set_bone_pose_scale(bone_idx, Vector3(CommandParams.json_float(s, "x", 1), CommandParams.json_float(s, "y", 1), CommandParams.json_float(s, "z", 1)))
|
|
273
|
+
respond({"success": true, "action": "set", "bone_index": bone_idx, "bone_name": skel.get_bone_name(bone_idx)})
|
|
274
|
+
_:
|
|
275
|
+
respond({"error": "Unknown bone action: %s. Use list, get, or set" % action})
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# An AnimationTree only exposes a playback object when its root is a state
|
|
279
|
+
# machine; every other root reports the parameter as null.
|
|
280
|
+
func _state_machine_playback(tree: AnimationTree) -> AnimationNodeStateMachinePlayback:
|
|
281
|
+
var playback: Variant = tree.get("parameters/playback")
|
|
282
|
+
if playback is AnimationNodeStateMachinePlayback:
|
|
283
|
+
return playback
|
|
284
|
+
return null
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
func _resolve_bone_index(skel: Skeleton3D, params: Dictionary) -> int:
|
|
288
|
+
if params.has("bone_index"):
|
|
289
|
+
return CommandParams.to_int(params["bone_index"])
|
|
290
|
+
if params.has("bone_name"):
|
|
291
|
+
return skel.find_bone(CommandParams.json_string(params, "bone_name"))
|
|
292
|
+
return -1
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# --- Viewport ---
|
|
296
|
+
# ==========================================================================
|
|
297
|
+
# Batch 1: Networking + Input + System + Signals + Script
|
|
298
|
+
# ==========================================================================
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
func _cmd_animation_tree(params: Dictionary) -> void:
|
|
302
|
+
var reader := CommandParams.new(params)
|
|
303
|
+
var node: Node = require_node(reader)
|
|
304
|
+
var action: String = reader.optional_enum("action", "get_state", ["travel", "set_param", "get_state"])
|
|
305
|
+
if params_invalid(reader):
|
|
306
|
+
return
|
|
307
|
+
if not node is AnimationTree:
|
|
308
|
+
reader.fail("node_path must reference an AnimationTree", {"param": "node_path", "reason": "invalid_value", "class": node.get_class()})
|
|
309
|
+
send_params_error(reader)
|
|
310
|
+
return
|
|
311
|
+
var tree: AnimationTree = node as AnimationTree
|
|
312
|
+
match action:
|
|
313
|
+
"travel":
|
|
314
|
+
var state_name: String = CommandParams.json_string(params, "state_name")
|
|
315
|
+
var playback: AnimationNodeStateMachinePlayback = _state_machine_playback(tree)
|
|
316
|
+
if playback != null:
|
|
317
|
+
playback.travel(state_name)
|
|
318
|
+
respond({"success": true, "action": "travel", "state": state_name})
|
|
319
|
+
"set_param":
|
|
320
|
+
var param_name: String = CommandParams.json_string(params, "param_name")
|
|
321
|
+
var param_value: Variant = params.get("param_value", 0)
|
|
322
|
+
tree.set("parameters/" + param_name, param_value)
|
|
323
|
+
respond({"success": true, "action": "set_param", "param": param_name})
|
|
324
|
+
"get_state":
|
|
325
|
+
var playback: AnimationNodeStateMachinePlayback = _state_machine_playback(tree)
|
|
326
|
+
var current: String = ""
|
|
327
|
+
if playback != null:
|
|
328
|
+
current = playback.get_current_node()
|
|
329
|
+
respond({"success": true, "action": "get_state", "current": current})
|
|
330
|
+
_:
|
|
331
|
+
respond({"error": "Unknown animation_tree action: %s" % action})
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
func _cmd_animation_control(params: Dictionary) -> void:
|
|
335
|
+
var reader := CommandParams.new(params)
|
|
336
|
+
var node: Node = require_node(reader)
|
|
337
|
+
var action: String = reader.optional_enum("action", "get_info", ["seek", "queue", "set_speed", "stop", "get_info"])
|
|
338
|
+
if params_invalid(reader):
|
|
339
|
+
return
|
|
340
|
+
if not node is AnimationPlayer:
|
|
341
|
+
reader.fail("node_path must reference an AnimationPlayer", {"param": "node_path", "reason": "invalid_value", "class": node.get_class()})
|
|
342
|
+
send_params_error(reader)
|
|
343
|
+
return
|
|
344
|
+
var player: AnimationPlayer = node as AnimationPlayer
|
|
345
|
+
match action:
|
|
346
|
+
"seek":
|
|
347
|
+
if player.current_animation.is_empty():
|
|
348
|
+
reader.fail("Cannot seek without a current animation", {"param": "node_path", "reason": "invalid_state"})
|
|
349
|
+
var max_position: float = player.current_animation_length if not player.current_animation.is_empty() else INF
|
|
350
|
+
var pos: float = reader.required_number("position", 0.0, max_position)
|
|
351
|
+
if params_invalid(reader):
|
|
352
|
+
return
|
|
353
|
+
player.seek(pos)
|
|
354
|
+
respond({"success": true, "action": "seek", "position": pos})
|
|
355
|
+
"queue":
|
|
356
|
+
var anim: String = reader.required_string("animation_name")
|
|
357
|
+
if not player.has_animation(anim):
|
|
358
|
+
reader.fail("Animation not found: %s" % anim, {"param": "animation_name", "reason": "not_found", "value": anim})
|
|
359
|
+
if params_invalid(reader):
|
|
360
|
+
return
|
|
361
|
+
player.queue(anim)
|
|
362
|
+
respond({"success": true, "action": "queue", "animation": anim})
|
|
363
|
+
"set_speed":
|
|
364
|
+
player.speed_scale = reader.required_number("speed")
|
|
365
|
+
if params_invalid(reader):
|
|
366
|
+
return
|
|
367
|
+
respond({"success": true, "action": "set_speed", "speed": player.speed_scale})
|
|
368
|
+
"stop":
|
|
369
|
+
player.stop()
|
|
370
|
+
respond({"success": true, "action": "stop"})
|
|
371
|
+
"get_info":
|
|
372
|
+
var anims: PackedStringArray = player.get_animation_list()
|
|
373
|
+
respond({"success": true, "action": "get_info", "current": player.current_animation, "playing": player.is_playing(), "animations": Array(anims), "queued": Array(player.get_queue()), "speed_scale": player.speed_scale, "position": player.current_animation_position})
|
|
374
|
+
_:
|
|
375
|
+
respond({"error": "Unknown animation_control action: %s" % action})
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
func _cmd_skeleton_ik(params: Dictionary) -> void:
|
|
379
|
+
var reader := CommandParams.new(params)
|
|
380
|
+
var node: Node = require_node(reader)
|
|
381
|
+
var action: String = reader.optional_enum("action", "start", ["start", "stop", "set_target"])
|
|
382
|
+
if params_invalid(reader):
|
|
383
|
+
return
|
|
384
|
+
if not node is SkeletonIK3D:
|
|
385
|
+
reader.fail("node_path must reference a SkeletonIK3D", {"param": "node_path", "reason": "invalid_value", "class": node.get_class()})
|
|
386
|
+
send_params_error(reader)
|
|
387
|
+
return
|
|
388
|
+
var ik: SkeletonIK3D = node as SkeletonIK3D
|
|
389
|
+
match action:
|
|
390
|
+
"start":
|
|
391
|
+
ik.start()
|
|
392
|
+
respond({"success": true, "action": "start"})
|
|
393
|
+
"stop":
|
|
394
|
+
ik.stop()
|
|
395
|
+
respond({"success": true, "action": "stop"})
|
|
396
|
+
"set_target":
|
|
397
|
+
var t: Dictionary = reader.required_dictionary("target")
|
|
398
|
+
if params_invalid(reader):
|
|
399
|
+
return
|
|
400
|
+
var target_tf: Transform3D = Transform3D.IDENTITY
|
|
401
|
+
target_tf.origin = Vector3(CommandParams.json_float(t, "x", 0), CommandParams.json_float(t, "y", 0), CommandParams.json_float(t, "z", 0))
|
|
402
|
+
ik.target = target_tf
|
|
403
|
+
respond({"success": true, "action": "set_target"})
|
|
404
|
+
_:
|
|
405
|
+
respond({"error": "Unknown skeleton_ik action: %s" % action})
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
func _cmd_audio_effect(params: Dictionary) -> void:
|
|
409
|
+
var reader := CommandParams.new(params)
|
|
410
|
+
var bus_name: String = params.get("bus_name", "Master")
|
|
411
|
+
var action: String = reader.optional_enum("action", "list", ["list", "add", "remove", "configure"])
|
|
412
|
+
if params_invalid(reader):
|
|
413
|
+
return
|
|
414
|
+
var bus_idx: int = AudioServer.get_bus_index(bus_name)
|
|
415
|
+
if bus_idx < 0:
|
|
416
|
+
respond({"error": "Audio bus not found: %s" % bus_name})
|
|
417
|
+
return
|
|
418
|
+
match action:
|
|
419
|
+
"list":
|
|
420
|
+
var effects: Array = []
|
|
421
|
+
for i in AudioServer.get_bus_effect_count(bus_idx):
|
|
422
|
+
var eff: AudioEffect = AudioServer.get_bus_effect(bus_idx, i)
|
|
423
|
+
effects.append({"index": i, "type": eff.get_class(), "enabled": AudioServer.is_bus_effect_enabled(bus_idx, i)})
|
|
424
|
+
respond({"success": true, "action": "list", "bus": bus_name, "effects": effects})
|
|
425
|
+
"add":
|
|
426
|
+
var effect_type: String = params.get("effect_type", "reverb")
|
|
427
|
+
var effect: AudioEffect
|
|
428
|
+
match effect_type:
|
|
429
|
+
"reverb": effect = AudioEffectReverb.new()
|
|
430
|
+
"delay": effect = AudioEffectDelay.new()
|
|
431
|
+
"chorus": effect = AudioEffectChorus.new()
|
|
432
|
+
"eq": effect = AudioEffectEQ6.new()
|
|
433
|
+
"compressor": effect = AudioEffectCompressor.new()
|
|
434
|
+
"limiter": effect = AudioEffectLimiter.new()
|
|
435
|
+
_:
|
|
436
|
+
respond({"error": "Unknown effect type: %s" % effect_type})
|
|
437
|
+
return
|
|
438
|
+
AudioServer.add_bus_effect(bus_idx, effect)
|
|
439
|
+
respond({"success": true, "action": "add", "effect_type": effect_type, "index": AudioServer.get_bus_effect_count(bus_idx) - 1})
|
|
440
|
+
"remove":
|
|
441
|
+
var idx: int = CommandParams.json_int(params, "index", 0)
|
|
442
|
+
if idx < 0 or idx >= AudioServer.get_bus_effect_count(bus_idx):
|
|
443
|
+
respond({"error": "Effect index out of range: %d" % idx})
|
|
444
|
+
return
|
|
445
|
+
AudioServer.remove_bus_effect(bus_idx, idx)
|
|
446
|
+
respond({"success": true, "action": "remove", "index": idx})
|
|
447
|
+
"configure":
|
|
448
|
+
var idx: int = CommandParams.json_int(params, "index", 0)
|
|
449
|
+
if idx < 0 or idx >= AudioServer.get_bus_effect_count(bus_idx):
|
|
450
|
+
respond({"error": "Effect index out of range: %d" % idx})
|
|
451
|
+
return
|
|
452
|
+
var eff: AudioEffect = AudioServer.get_bus_effect(bus_idx, idx)
|
|
453
|
+
var applied: Array = []
|
|
454
|
+
var props: Dictionary = CommandParams.json_dictionary(params, "properties")
|
|
455
|
+
for key: Variant in props:
|
|
456
|
+
eff.set(StringName(str(key)), props[key])
|
|
457
|
+
applied.append(str(key))
|
|
458
|
+
if params.has("enabled"):
|
|
459
|
+
AudioServer.set_bus_effect_enabled(bus_idx, idx, CommandParams.to_bool(params["enabled"]))
|
|
460
|
+
applied.append("enabled")
|
|
461
|
+
respond({"success": true, "action": "configure", "index": idx, "applied": applied})
|
|
462
|
+
_:
|
|
463
|
+
respond({"error": "Unknown audio_effect action: %s" % action})
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
func _cmd_audio_bus_layout(params: Dictionary) -> void:
|
|
467
|
+
var reader := CommandParams.new(params)
|
|
468
|
+
var action: String = reader.optional_enum("action", "list", ["list", "add", "remove", "move", "set_send"])
|
|
469
|
+
if params_invalid(reader):
|
|
470
|
+
return
|
|
471
|
+
match action:
|
|
472
|
+
"list":
|
|
473
|
+
var buses: Array = []
|
|
474
|
+
for i in AudioServer.bus_count:
|
|
475
|
+
buses.append({"index": i, "name": AudioServer.get_bus_name(i), "volume": AudioServer.get_bus_volume_db(i), "mute": AudioServer.is_bus_mute(i), "solo": AudioServer.is_bus_solo(i), "send": AudioServer.get_bus_send(i), "effect_count": AudioServer.get_bus_effect_count(i)})
|
|
476
|
+
respond({"success": true, "action": "list", "buses": buses})
|
|
477
|
+
"add":
|
|
478
|
+
var bus_name: String = reader.required_string("bus_name")
|
|
479
|
+
if bus_name.is_empty():
|
|
480
|
+
reader.fail("bus_name must not be empty", {"param": "bus_name", "reason": "invalid_value"})
|
|
481
|
+
if AudioServer.get_bus_index(bus_name) >= 0:
|
|
482
|
+
reader.fail("Audio bus already exists: %s" % bus_name, {"param": "bus_name", "reason": "already_exists", "value": bus_name})
|
|
483
|
+
if params_invalid(reader):
|
|
484
|
+
return
|
|
485
|
+
AudioServer.add_bus()
|
|
486
|
+
var idx: int = AudioServer.bus_count - 1
|
|
487
|
+
AudioServer.set_bus_name(idx, bus_name)
|
|
488
|
+
respond({"success": true, "action": "add", "bus_name": bus_name, "index": idx})
|
|
489
|
+
"remove":
|
|
490
|
+
var bus_name: String = reader.required_string("bus_name")
|
|
491
|
+
var idx: int = AudioServer.get_bus_index(bus_name)
|
|
492
|
+
if idx <= 0:
|
|
493
|
+
reader.fail("Cannot remove bus: %s" % bus_name, {"param": "bus_name", "reason": "not_found_or_protected", "value": bus_name})
|
|
494
|
+
send_params_error(reader)
|
|
495
|
+
return
|
|
496
|
+
AudioServer.remove_bus(idx)
|
|
497
|
+
respond({"success": true, "action": "remove", "bus_name": bus_name})
|
|
498
|
+
"move":
|
|
499
|
+
var bus_name: String = reader.required_string("bus_name")
|
|
500
|
+
var from_index: int = AudioServer.get_bus_index(bus_name)
|
|
501
|
+
var to_index: int = reader.required_int("index", 1, AudioServer.bus_count - 1)
|
|
502
|
+
if from_index <= 0:
|
|
503
|
+
reader.fail("Cannot move bus: %s" % bus_name, {"param": "bus_name", "reason": "not_found_or_protected", "value": bus_name})
|
|
504
|
+
if params_invalid(reader):
|
|
505
|
+
return
|
|
506
|
+
AudioServer.move_bus(from_index, to_index)
|
|
507
|
+
respond({"success": true, "action": "move", "bus_name": bus_name, "index": AudioServer.get_bus_index(bus_name)})
|
|
508
|
+
"set_send":
|
|
509
|
+
var bus_name: String = reader.required_string("bus_name")
|
|
510
|
+
var send_to: String = reader.required_string("send_to")
|
|
511
|
+
var idx: int = AudioServer.get_bus_index(bus_name)
|
|
512
|
+
if idx <= 0:
|
|
513
|
+
reader.fail("Bus not found or protected: %s" % bus_name, {"param": "bus_name", "reason": "not_found_or_protected", "value": bus_name})
|
|
514
|
+
if AudioServer.get_bus_index(send_to) < 0:
|
|
515
|
+
reader.fail("Send target not found: %s" % send_to, {"param": "send_to", "reason": "not_found", "value": send_to})
|
|
516
|
+
if bus_name == send_to:
|
|
517
|
+
reader.fail("An audio bus cannot send to itself", {"param": "send_to", "reason": "invalid_value", "value": send_to})
|
|
518
|
+
if params_invalid(reader):
|
|
519
|
+
return
|
|
520
|
+
AudioServer.set_bus_send(idx, send_to)
|
|
521
|
+
respond({"success": true, "action": "set_send", "bus": bus_name, "send_to": send_to})
|
|
522
|
+
_:
|
|
523
|
+
respond({"error": "Unknown audio_bus_layout action: %s" % action})
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
func _cmd_audio_spatial(params: Dictionary) -> void:
|
|
527
|
+
var reader := CommandParams.new(params)
|
|
528
|
+
var node: Node = require_node(reader)
|
|
529
|
+
var action: String = reader.optional_enum("action", "get_info", ["get_info", "configure"])
|
|
530
|
+
if params_invalid(reader):
|
|
531
|
+
return
|
|
532
|
+
if not node is AudioStreamPlayer3D:
|
|
533
|
+
reader.fail("node_path must reference an AudioStreamPlayer3D", {"param": "node_path", "reason": "invalid_value", "class": node.get_class()})
|
|
534
|
+
send_params_error(reader)
|
|
535
|
+
return
|
|
536
|
+
var player: AudioStreamPlayer3D = node as AudioStreamPlayer3D
|
|
537
|
+
if action == "get_info":
|
|
538
|
+
respond({"success": true, "max_distance": player.max_distance, "unit_size": player.unit_size, "max_db": player.max_db, "attenuation_model": _attenuation_model_name(player.attenuation_model), "playing": player.playing})
|
|
539
|
+
return
|
|
540
|
+
if params.has("max_distance"):
|
|
541
|
+
player.max_distance = CommandParams.to_float(params["max_distance"])
|
|
542
|
+
if params.has("unit_size"):
|
|
543
|
+
player.unit_size = CommandParams.to_float(params["unit_size"])
|
|
544
|
+
if params.has("max_db"):
|
|
545
|
+
player.max_db = CommandParams.to_float(params["max_db"])
|
|
546
|
+
if params.has("attenuation_model"):
|
|
547
|
+
var attenuation_model: String = CommandParams.json_string(params, "attenuation_model")
|
|
548
|
+
match attenuation_model:
|
|
549
|
+
"inverse": player.attenuation_model = AudioStreamPlayer3D.ATTENUATION_INVERSE_DISTANCE
|
|
550
|
+
"inverse_square": player.attenuation_model = AudioStreamPlayer3D.ATTENUATION_INVERSE_SQUARE_DISTANCE
|
|
551
|
+
"logarithmic": player.attenuation_model = AudioStreamPlayer3D.ATTENUATION_LOGARITHMIC
|
|
552
|
+
_:
|
|
553
|
+
respond({"error": "Unknown attenuation model: %s" % attenuation_model})
|
|
554
|
+
return
|
|
555
|
+
respond({"success": true, "action": "configure"})
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
func _attenuation_model_name(model: AudioStreamPlayer3D.AttenuationModel) -> String:
|
|
559
|
+
match model:
|
|
560
|
+
AudioStreamPlayer3D.ATTENUATION_INVERSE_SQUARE_DISTANCE: return "inverse_square"
|
|
561
|
+
AudioStreamPlayer3D.ATTENUATION_LOGARITHMIC: return "logarithmic"
|
|
562
|
+
AudioStreamPlayer3D.ATTENUATION_DISABLED: return "disabled"
|
|
563
|
+
_: return "inverse"
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# ==========================================================================
|
|
567
|
+
# Batch 4: Locale (runtime)
|
|
568
|
+
# ==========================================================================
|