breakpoint-mcp 1.0.0 → 1.2.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.
@@ -0,0 +1,4578 @@
1
+ @tool
2
+ extends RefCounted
3
+ ## Request handlers for the Breakpoint MCP.
4
+ ##
5
+ ## Every handler returns a plain Dictionary that becomes the JSON-RPC `result`.
6
+ ## Errors are raised via `_err(code, message)`. All edit-time mutations are
7
+ ## wrapped in the EditorUndoRedoManager so a human can Ctrl-Z anything the assistant did.
8
+
9
+ const Codec := preload("res://addons/breakpoint_mcp/variant_json.gd")
10
+ const ADDON_VERSION := "1.0.0"
11
+
12
+ var _plugin: EditorPlugin
13
+
14
+
15
+ func setup(plugin: EditorPlugin) -> void:
16
+ _plugin = plugin
17
+
18
+
19
+ ## Dispatch a method name + params to a handler. Returns {ok, result|error}.
20
+ func dispatch(method: String, params: Dictionary) -> Dictionary:
21
+ match method:
22
+ "ping":
23
+ return _ok(_ping())
24
+ "editor.get_state":
25
+ return _ok(_editor_get_state())
26
+ "edit.undo":
27
+ return _edit_undo(params)
28
+ "edit.redo":
29
+ return _edit_redo(params)
30
+ "project.get_info":
31
+ return _ok(_project_get_info())
32
+ "project.get_setting":
33
+ return _project_get_setting(params)
34
+ "project.set_setting":
35
+ return _project_set_setting(params)
36
+ "scene.get_tree":
37
+ return _scene_get_tree(params)
38
+ "scene.open":
39
+ return _scene_open(params)
40
+ "scene.save":
41
+ return _scene_save()
42
+ "scene.new":
43
+ return _scene_new(params)
44
+ "scene.list_open":
45
+ return _ok(_scene_list_open())
46
+ "scene.reload":
47
+ return _scene_reload(params)
48
+ "scene.close":
49
+ return _scene_close(params)
50
+ "scene.pack":
51
+ return _scene_pack(params)
52
+ "scene.get_dependencies":
53
+ return _scene_get_dependencies(params)
54
+ "scene.save_as":
55
+ return _scene_save_as(params)
56
+ "node.add":
57
+ return _node_add(params)
58
+ "node.delete":
59
+ return _node_delete(params)
60
+ "node.rename":
61
+ return _node_rename(params)
62
+ "node.reparent":
63
+ return _node_reparent(params)
64
+ "node.set_property":
65
+ return _node_set_property(params)
66
+ "node.get_property":
67
+ return _node_get_property(params)
68
+ "node.duplicate":
69
+ return _node_duplicate(params)
70
+ "node.get_children":
71
+ return _node_get_children(params)
72
+ "node.find":
73
+ return _node_find(params)
74
+ "node.list_groups":
75
+ return _node_list_groups(params)
76
+ "node.add_to_group":
77
+ return _node_add_to_group(params)
78
+ "node.remove_from_group":
79
+ return _node_remove_from_group(params)
80
+ "node.instantiate_scene":
81
+ return _node_instantiate_scene(params)
82
+ "node.move_child":
83
+ return _node_move_child(params)
84
+ "node.change_type":
85
+ return _node_change_type(params)
86
+ "node.set_owner":
87
+ return _node_set_owner(params)
88
+ "node.call_method":
89
+ return _node_call_method(params)
90
+ "node.get_path":
91
+ return _node_get_path(params)
92
+ "node.list_properties":
93
+ return _node_list_properties(params)
94
+ "signal.list":
95
+ return _signal_list(params)
96
+ "signal.list_connections":
97
+ return _signal_list_connections(params)
98
+ "signal.connect":
99
+ return _signal_connect(params)
100
+ "signal.disconnect":
101
+ return _signal_disconnect(params)
102
+ "signal.add_user_signal":
103
+ return _signal_add_user_signal(params)
104
+ "signal.emit":
105
+ return _signal_emit(params)
106
+ "resource.create":
107
+ return _resource_create(params)
108
+ "resource.load":
109
+ return _resource_load(params)
110
+ "resource.save":
111
+ return _resource_save(params)
112
+ "resource.duplicate":
113
+ return _resource_duplicate(params)
114
+ "resource.get_property":
115
+ return _resource_get_property(params)
116
+ "resource.set_property":
117
+ return _resource_set_property(params)
118
+ "resource.get_import_settings":
119
+ return _resource_get_import_settings(params)
120
+ "resource.set_import_settings":
121
+ return _resource_set_import_settings(params)
122
+ "filesystem.list":
123
+ return _filesystem_list(params)
124
+ "filesystem.scan":
125
+ return _filesystem_scan(params)
126
+ "filesystem.move":
127
+ return _filesystem_move(params)
128
+ "filesystem.create_dir":
129
+ return _filesystem_create_dir(params)
130
+ "asset.gen_placeholder":
131
+ return _asset_gen_placeholder(params)
132
+ "asset.import":
133
+ return _asset_import(params)
134
+ "mp.add_spawner":
135
+ return _mp_add_spawner(params)
136
+ "mp.add_synchronizer":
137
+ return _mp_add_synchronizer(params)
138
+ "mp.set_authority":
139
+ return _mp_set_authority(params)
140
+ "mp.write_script":
141
+ return _mp_write_script(params)
142
+ "backend.detect":
143
+ return _backend_detect(params)
144
+ "anim.player_create":
145
+ return _anim_player_create(params)
146
+ "anim.create":
147
+ return _anim_create(params)
148
+ "anim.delete":
149
+ return _anim_delete(params)
150
+ "anim.add_track":
151
+ return _anim_add_track(params)
152
+ "anim.insert_key":
153
+ return _anim_insert_key(params)
154
+ "anim.remove_key":
155
+ return _anim_remove_key(params)
156
+ "anim.set_length":
157
+ return _anim_set_length(params)
158
+ "anim.set_loop":
159
+ return _anim_set_loop(params)
160
+ "anim.get_track_keys":
161
+ return _anim_get_track_keys(params)
162
+ "anim.list":
163
+ return _anim_list(params)
164
+ "anim.tree_create":
165
+ return _anim_tree_create(params)
166
+ "anim.tree_add_node":
167
+ return _anim_tree_add_node(params)
168
+ "anim.statemachine_add_state":
169
+ return _anim_statemachine_add_state(params)
170
+ "anim.statemachine_add_transition":
171
+ return _anim_statemachine_add_transition(params)
172
+ "tileset.create":
173
+ return _tileset_create(params)
174
+ "tileset.add_source":
175
+ return _tileset_add_source(params)
176
+ "tileset.add_tile":
177
+ return _tileset_add_tile(params)
178
+ "tileset.set_tile_collision":
179
+ return _tileset_set_tile_collision(params)
180
+ "tilemaplayer.create":
181
+ return _tilemaplayer_create(params)
182
+ "tilemap.set_cell":
183
+ return _tilemap_set_cell(params)
184
+ "tilemap.set_cells_rect":
185
+ return _tilemap_set_cells_rect(params)
186
+ "tilemap.get_cell":
187
+ return _tilemap_get_cell(params)
188
+ "tilemap.clear":
189
+ return _tilemap_clear(params)
190
+ "body.create":
191
+ return _body_create(params)
192
+ "collisionshape.add":
193
+ return _collisionshape_add(params)
194
+ "body.set_collision_layer":
195
+ return _body_set_collision_layer(params)
196
+ "body.set_collision_mask":
197
+ return _body_set_collision_mask(params)
198
+ "area.set_monitoring":
199
+ return _area_set_monitoring(params)
200
+ "area.set_gravity":
201
+ return _area_set_gravity(params)
202
+ "joint.create":
203
+ return _joint_create(params)
204
+ "joint.set_bodies":
205
+ return _joint_set_bodies(params)
206
+ "collisionpolygon.add":
207
+ return _collisionpolygon_add(params)
208
+ "rigidbody.set_properties":
209
+ return _rigidbody_set_properties(params)
210
+ "body.set_physics_material":
211
+ return _body_set_physics_material(params)
212
+ "physics.set_gravity":
213
+ return _physics_set_gravity(params)
214
+ "particles.create":
215
+ return _particles_create(params)
216
+ "particles.set_process_material":
217
+ return _particles_set_process_material(params)
218
+ "particles.set_amount":
219
+ return _particles_set_amount(params)
220
+ "particles.set_lifetime":
221
+ return _particles_set_lifetime(params)
222
+ "particles.set_emitting":
223
+ return _particles_set_emitting(params)
224
+ "particles.set_texture":
225
+ return _particles_set_texture(params)
226
+ "shader.create":
227
+ return _shader_create(params)
228
+ "shader.set_code":
229
+ return _shader_set_code(params)
230
+ "shadermaterial.create":
231
+ return _shadermaterial_create(params)
232
+ "shadermaterial.set_shader":
233
+ return _shadermaterial_set_shader(params)
234
+ "shadermaterial.set_param":
235
+ return _shadermaterial_set_param(params)
236
+ "audio.player_create":
237
+ return _audio_player_create(params)
238
+ "audio.set_stream":
239
+ return _audio_set_stream(params)
240
+ "audio.bus_add":
241
+ return _audio_bus_add(params)
242
+ "audio.bus_add_effect":
243
+ return _audio_bus_add_effect(params)
244
+ "audio.bus_set_volume":
245
+ return _audio_bus_set_volume(params)
246
+ "audio.set_bus_layout":
247
+ return _audio_set_bus_layout(params)
248
+ "control.create":
249
+ return _control_create(params)
250
+ "container.add_child":
251
+ return _container_add_child(params)
252
+ "control.set_anchors":
253
+ return _control_set_anchors(params)
254
+ "control.set_layout_preset":
255
+ return _control_set_layout_preset(params)
256
+ "control.set_size_flags":
257
+ return _control_set_size_flags(params)
258
+ "control.set_theme":
259
+ return _control_set_theme(params)
260
+ "theme.create":
261
+ return _theme_create(params)
262
+ "theme.set_color":
263
+ return _theme_set_color(params)
264
+ "theme.set_font":
265
+ return _theme_set_font(params)
266
+ "theme.set_stylebox":
267
+ return _theme_set_stylebox(params)
268
+ "theme.set_constant":
269
+ return _theme_set_constant(params)
270
+ "meshinstance.create":
271
+ return _meshinstance_create(params)
272
+ "mesh.set_surface_material":
273
+ return _mesh_set_surface_material(params)
274
+ "primitive_mesh.create":
275
+ return _primitive_mesh_create(params)
276
+ "light.create":
277
+ return _light_create(params)
278
+ "camera.create":
279
+ return _camera_create(params)
280
+ "environment.create":
281
+ return _environment_create(params)
282
+ "environment.set_sky":
283
+ return _environment_set_sky(params)
284
+ "csg.create":
285
+ return _csg_create(params)
286
+ "navregion.create":
287
+ return _navregion_create(params)
288
+ "navagent.configure":
289
+ return _navagent_configure(params)
290
+ "inputmap.add_action":
291
+ return _inputmap_add_action(params)
292
+ "inputmap.add_event":
293
+ return _inputmap_add_event(params)
294
+ "inputmap.list":
295
+ return _inputmap_list(params)
296
+ "inputmap.erase_action":
297
+ return _inputmap_erase_action(params)
298
+ "project.add_autoload":
299
+ return _project_add_autoload(params)
300
+ "project.remove_autoload":
301
+ return _project_remove_autoload(params)
302
+ "project.add_export_preset":
303
+ return _project_add_export_preset(params)
304
+ "project.set_main_scene":
305
+ return _project_set_main_scene(params)
306
+ "project.list_settings":
307
+ return _project_list_settings(params)
308
+ "editorsettings.get_set":
309
+ return _editorsettings_get_set(params)
310
+ "test.detect":
311
+ return _test_detect(params)
312
+ "test.list":
313
+ return _test_list(params)
314
+ "selection.get":
315
+ return _ok(_selection_get())
316
+ "selection.set":
317
+ return _selection_set(params)
318
+ "classdb.get_class":
319
+ return _classdb_get_class(params)
320
+ "classdb.reference":
321
+ return _classdb_reference(params)
322
+ "docs.search":
323
+ return _docs_search(params)
324
+ "screenshot.editor_viewport":
325
+ return _screenshot(params)
326
+ _:
327
+ return _err("unknown_method", "No such method: %s" % method)
328
+
329
+
330
+ # ---------------------------------------------------------------- helpers ----
331
+
332
+ func _ok(result: Variant) -> Dictionary:
333
+ return {"ok": true, "result": result}
334
+
335
+
336
+ func _err(code: String, message: String) -> Dictionary:
337
+ return {"ok": false, "error": {"code": code, "message": message}}
338
+
339
+
340
+ func _edited_root() -> Node:
341
+ return EditorInterface.get_edited_scene_root()
342
+
343
+
344
+ ## Resolve a node path relative to the edited scene root.
345
+ ## "" or "." → the root itself; otherwise a path like "Player/Sprite2D".
346
+ func _resolve(root: Node, path: String) -> Node:
347
+ if root == null:
348
+ return null
349
+ if path == "" or path == "." or path == "/root":
350
+ return root
351
+ if root.has_node(path):
352
+ return root.get_node(path)
353
+ return null
354
+
355
+
356
+ func _path_of(root: Node, node: Node) -> String:
357
+ if node == root:
358
+ return "."
359
+ return String(root.get_path_to(node))
360
+
361
+
362
+ # ------------------------------------------------------------- handlers ------
363
+
364
+ func _ping() -> Dictionary:
365
+ return {
366
+ "pong": true,
367
+ "addon_version": ADDON_VERSION,
368
+ "godot": Engine.get_version_info().get("string", ""),
369
+ }
370
+
371
+
372
+ func _editor_get_state() -> Dictionary:
373
+ var root := _edited_root()
374
+ var selection: Array = []
375
+ if root:
376
+ for n in EditorInterface.get_selection().get_selected_nodes():
377
+ selection.append(_path_of(root, n))
378
+ return {
379
+ "has_open_scene": root != null,
380
+ "edited_scene_root": (_path_of(root, root) if root else null),
381
+ "edited_scene_path": (root.scene_file_path if root else null),
382
+ "root_type": (root.get_class() if root else null),
383
+ "selection": selection,
384
+ "godot": Engine.get_version_info().get("string", ""),
385
+ }
386
+
387
+
388
+ func _edit_undo(params: Dictionary) -> Dictionary:
389
+ return _edit_history_step(params, true)
390
+
391
+
392
+ func _edit_redo(params: Dictionary) -> Dictionary:
393
+ return _edit_history_step(params, false)
394
+
395
+
396
+ ## Drive the editor's undo/redo history. `is_undo` picks the direction.
397
+ ## `scope` selects which history: "scene" (default) resolves the edited scene's
398
+ ## history via EditorUndoRedoManager.get_object_history_id(root) — the same
399
+ ## routing the node_* mutators commit into — while "global" targets the
400
+ ## editor-wide GLOBAL_HISTORY. The concrete UndoRedo is fetched with
401
+ ## get_history_undo_redo(id) and stepped directly; that is the only
402
+ ## scripting-exposed route to a programmatic Ctrl-Z (validated on Godot 4.7).
403
+ func _edit_history_step(params: Dictionary, is_undo: bool) -> Dictionary:
404
+ var ur := _plugin.get_undo_redo()
405
+ var scope := String(params.get("scope", "scene"))
406
+ var hid := _history_id_for_scope(ur, scope)
407
+ if hid == EditorUndoRedoManager.INVALID_HISTORY:
408
+ return _err("no_history", "No undo/redo history for scope '%s'" % scope)
409
+ var hist := ur.get_history_undo_redo(hid)
410
+ if hist == null:
411
+ return _err("no_history", "No UndoRedo for history %d (scope '%s')" % [hid, scope])
412
+ var performed := false
413
+ var action_name := ""
414
+ if is_undo:
415
+ if hist.has_undo():
416
+ action_name = hist.get_current_action_name()
417
+ performed = hist.undo()
418
+ else:
419
+ if hist.has_redo():
420
+ performed = hist.redo()
421
+ action_name = hist.get_current_action_name()
422
+ return _ok({
423
+ "performed": performed,
424
+ "direction": ("undo" if is_undo else "redo"),
425
+ "action": (action_name if performed else ""),
426
+ "has_undo": hist.has_undo(),
427
+ "has_redo": hist.has_redo(),
428
+ "history_id": hid,
429
+ "scope": scope,
430
+ })
431
+
432
+
433
+ ## Resolve the target undo-history id for a scope string.
434
+ func _history_id_for_scope(ur: EditorUndoRedoManager, scope: String) -> int:
435
+ if scope == "global":
436
+ return EditorUndoRedoManager.GLOBAL_HISTORY
437
+ var root := _edited_root()
438
+ if root != null:
439
+ return ur.get_object_history_id(root)
440
+ return EditorUndoRedoManager.GLOBAL_HISTORY
441
+
442
+
443
+ func _project_get_info() -> Dictionary:
444
+ return {
445
+ "name": ProjectSettings.get_setting("application/config/name", ""),
446
+ "main_scene": ProjectSettings.get_setting("application/run/main_scene", ""),
447
+ "project_root": ProjectSettings.globalize_path("res://"),
448
+ "godot": Engine.get_version_info().get("string", ""),
449
+ "features": Array(ProjectSettings.get_setting("application/config/features", [])),
450
+ }
451
+
452
+
453
+ func _project_get_setting(params: Dictionary) -> Dictionary:
454
+ var key := String(params.get("name", ""))
455
+ if key == "" or not ProjectSettings.has_setting(key):
456
+ return _err("not_found", "Project setting not found: %s" % key)
457
+ return _ok({"name": key, "value": Codec.encode(ProjectSettings.get_setting(key))})
458
+
459
+
460
+ func _project_set_setting(params: Dictionary) -> Dictionary:
461
+ var key := String(params.get("name", ""))
462
+ if key == "":
463
+ return _err("bad_params", "Missing 'name'")
464
+ ProjectSettings.set_setting(key, Codec.decode(params.get("value")))
465
+ if bool(params.get("save", false)):
466
+ var e := ProjectSettings.save()
467
+ if e != OK:
468
+ return _err("save_failed", "ProjectSettings.save() returned %d" % e)
469
+ return _ok({"name": key, "saved": bool(params.get("save", false))})
470
+
471
+
472
+ func _serialize_node(node: Node, root: Node, depth: int, max_depth: int) -> Dictionary:
473
+ var script_path: Variant = null
474
+ var scr := node.get_script()
475
+ if scr and scr is Resource:
476
+ script_path = (scr as Resource).resource_path
477
+ var d := {
478
+ "name": String(node.name),
479
+ "type": node.get_class(),
480
+ "path": _path_of(root, node),
481
+ "script": script_path,
482
+ "child_count": node.get_child_count(),
483
+ }
484
+ if depth < max_depth and node.get_child_count() > 0:
485
+ var kids: Array = []
486
+ for c in node.get_children():
487
+ kids.append(_serialize_node(c, root, depth + 1, max_depth))
488
+ d["children"] = kids
489
+ return d
490
+
491
+
492
+ func _scene_get_tree(params: Dictionary) -> Dictionary:
493
+ var root := _edited_root()
494
+ if root == null:
495
+ return _err("no_scene", "No scene is currently open in the editor")
496
+ var max_depth := int(params.get("max_depth", 64))
497
+ return _ok(_serialize_node(root, root, 0, max_depth))
498
+
499
+
500
+ func _scene_open(params: Dictionary) -> Dictionary:
501
+ var path := String(params.get("path", ""))
502
+ if path == "" or not ResourceLoader.exists(path):
503
+ return _err("not_found", "Scene not found: %s" % path)
504
+ EditorInterface.open_scene_from_path(path)
505
+ return _ok({"opened": path})
506
+
507
+
508
+ func _scene_save() -> Dictionary:
509
+ var root := _edited_root()
510
+ if root == null:
511
+ return _err("no_scene", "No scene is open")
512
+ var e := EditorInterface.save_scene()
513
+ if e != OK:
514
+ return _err("save_failed", "save_scene() returned %d (has the scene ever been saved to a path?)" % e)
515
+ return _ok({"saved": root.scene_file_path})
516
+
517
+
518
+ func _scene_new(params: Dictionary) -> Dictionary:
519
+ var root_type := String(params.get("root_type", "Node"))
520
+ var path := String(params.get("path", ""))
521
+ if path == "":
522
+ return _err("bad_params", "'path' is required (e.g. res://scenes/new.tscn)")
523
+ if not ClassDB.can_instantiate(root_type):
524
+ return _err("bad_type", "Cannot instantiate class: %s" % root_type)
525
+ var root: Node = ClassDB.instantiate(root_type)
526
+ root.name = String(params.get("name", root_type))
527
+ var packed := PackedScene.new()
528
+ var e := packed.pack(root)
529
+ if e != OK:
530
+ return _err("pack_failed", "PackedScene.pack() returned %d" % e)
531
+ e = ResourceSaver.save(packed, path)
532
+ if e != OK:
533
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
534
+ EditorInterface.open_scene_from_path(path)
535
+ return _ok({"created": path, "root_type": root_type})
536
+
537
+
538
+ func _node_add(params: Dictionary) -> Dictionary:
539
+ var root := _edited_root()
540
+ if root == null:
541
+ return _err("no_scene", "No scene is open")
542
+ var parent := _resolve(root, String(params.get("parent_path", "")))
543
+ if parent == null:
544
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
545
+ var type := String(params.get("type", "Node"))
546
+ if not ClassDB.can_instantiate(type):
547
+ return _err("bad_type", "Cannot instantiate class: %s" % type)
548
+ var node: Node = ClassDB.instantiate(type)
549
+ node.name = String(params.get("name", type))
550
+ var ur := _plugin.get_undo_redo()
551
+ ur.create_action("Breakpoint: add %s" % node.name)
552
+ ur.add_do_method(parent, "add_child", node)
553
+ ur.add_do_method(node, "set_owner", root)
554
+ ur.add_do_reference(node)
555
+ ur.add_undo_method(parent, "remove_child", node)
556
+ ur.commit_action()
557
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": type})
558
+
559
+
560
+ func _node_delete(params: Dictionary) -> Dictionary:
561
+ var root := _edited_root()
562
+ if root == null:
563
+ return _err("no_scene", "No scene is open")
564
+ var node := _resolve(root, String(params.get("path", "")))
565
+ if node == null:
566
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
567
+ if node == root:
568
+ return _err("refused", "Refusing to delete the scene root")
569
+ var parent := node.get_parent()
570
+ var index := node.get_index()
571
+ var ur := _plugin.get_undo_redo()
572
+ ur.create_action("Breakpoint: delete %s" % node.name)
573
+ ur.add_do_method(parent, "remove_child", node)
574
+ ur.add_undo_method(parent, "add_child", node)
575
+ ur.add_undo_method(parent, "move_child", node, index)
576
+ ur.add_undo_method(node, "set_owner", root)
577
+ ur.add_undo_reference(node)
578
+ ur.commit_action()
579
+ return _ok({"deleted": String(params.get("path", ""))})
580
+
581
+
582
+ func _node_rename(params: Dictionary) -> Dictionary:
583
+ var root := _edited_root()
584
+ if root == null:
585
+ return _err("no_scene", "No scene is open")
586
+ var node := _resolve(root, String(params.get("path", "")))
587
+ if node == null:
588
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
589
+ var new_name := String(params.get("new_name", ""))
590
+ if new_name == "":
591
+ return _err("bad_params", "Missing 'new_name'")
592
+ var old_name := String(node.name)
593
+ var ur := _plugin.get_undo_redo()
594
+ ur.create_action("Breakpoint: rename %s -> %s" % [old_name, new_name])
595
+ ur.add_do_property(node, "name", new_name)
596
+ ur.add_undo_property(node, "name", old_name)
597
+ ur.commit_action()
598
+ return _ok({"path": _path_of(root, node), "name": String(node.name)})
599
+
600
+
601
+ func _node_reparent(params: Dictionary) -> Dictionary:
602
+ var root := _edited_root()
603
+ if root == null:
604
+ return _err("no_scene", "No scene is open")
605
+ var node := _resolve(root, String(params.get("path", "")))
606
+ if node == null:
607
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
608
+ if node == root:
609
+ return _err("refused", "Cannot reparent the scene root")
610
+ var new_parent := _resolve(root, String(params.get("new_parent_path", "")))
611
+ if new_parent == null:
612
+ return _err("bad_path", "New parent not found: %s" % params.get("new_parent_path", ""))
613
+ var keep := bool(params.get("keep_global_transform", true))
614
+ var old_parent := node.get_parent()
615
+ var old_index := node.get_index()
616
+ var ur := _plugin.get_undo_redo()
617
+ ur.create_action("Breakpoint: reparent %s" % node.name)
618
+ ur.add_do_method(node, "reparent", new_parent, keep)
619
+ ur.add_do_method(node, "set_owner", root)
620
+ ur.add_undo_method(node, "reparent", old_parent, keep)
621
+ ur.add_undo_method(old_parent, "move_child", node, old_index)
622
+ ur.add_undo_method(node, "set_owner", root)
623
+ ur.commit_action()
624
+ return _ok({"path": _path_of(root, node)})
625
+
626
+
627
+ func _node_set_property(params: Dictionary) -> Dictionary:
628
+ var root := _edited_root()
629
+ if root == null:
630
+ return _err("no_scene", "No scene is open")
631
+ var node := _resolve(root, String(params.get("path", "")))
632
+ if node == null:
633
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
634
+ var prop := String(params.get("property", ""))
635
+ if prop == "":
636
+ return _err("bad_params", "Missing 'property'")
637
+ var old_value: Variant = node.get(prop)
638
+ var new_value: Variant = Codec.decode(params.get("value"))
639
+ var ur := _plugin.get_undo_redo()
640
+ ur.create_action("Breakpoint: set %s.%s" % [node.name, prop])
641
+ ur.add_do_property(node, prop, new_value)
642
+ ur.add_undo_property(node, prop, old_value)
643
+ ur.commit_action()
644
+ return _ok({"path": _path_of(root, node), "property": prop, "value": Codec.encode(node.get(prop))})
645
+
646
+
647
+ func _node_get_property(params: Dictionary) -> Dictionary:
648
+ var root := _edited_root()
649
+ if root == null:
650
+ return _err("no_scene", "No scene is open")
651
+ var node := _resolve(root, String(params.get("path", "")))
652
+ if node == null:
653
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
654
+ var prop := String(params.get("property", ""))
655
+ if prop == "":
656
+ return _err("bad_params", "Missing 'property'")
657
+ return _ok({"path": _path_of(root, node), "property": prop, "value": Codec.encode(node.get(prop))})
658
+
659
+
660
+ func _selection_get() -> Dictionary:
661
+ var root := _edited_root()
662
+ var paths: Array = []
663
+ if root:
664
+ for n in EditorInterface.get_selection().get_selected_nodes():
665
+ paths.append(_path_of(root, n))
666
+ return {"selection": paths}
667
+
668
+
669
+ func _selection_set(params: Dictionary) -> Dictionary:
670
+ var root := _edited_root()
671
+ if root == null:
672
+ return _err("no_scene", "No scene is open")
673
+ var sel := EditorInterface.get_selection()
674
+ sel.clear()
675
+ var applied: Array = []
676
+ for p in params.get("paths", []):
677
+ var node := _resolve(root, String(p))
678
+ if node:
679
+ sel.add_node(node)
680
+ applied.append(String(p))
681
+ return _ok({"selection": applied})
682
+
683
+
684
+ func _classdb_get_class(params: Dictionary) -> Dictionary:
685
+ var cls := String(params.get("class_name", ""))
686
+ if cls == "" or not ClassDB.class_exists(cls):
687
+ return _err("not_found", "Class not found: %s" % cls)
688
+ var inherited := bool(params.get("include_inherited", false))
689
+ var no_inherit := not inherited
690
+ var methods: Array = []
691
+ for m in ClassDB.class_get_method_list(cls, no_inherit):
692
+ methods.append(String(m.get("name", "")))
693
+ var props: Array = []
694
+ for p in ClassDB.class_get_property_list(cls, no_inherit):
695
+ props.append(String(p.get("name", "")))
696
+ var signals: Array = []
697
+ for s in ClassDB.class_get_signal_list(cls, no_inherit):
698
+ signals.append(String(s.get("name", "")))
699
+ return _ok({
700
+ "class": cls,
701
+ "parent": ClassDB.get_parent_class(cls),
702
+ "can_instantiate": ClassDB.can_instantiate(cls),
703
+ "methods": methods,
704
+ "properties": props,
705
+ "signals": signals,
706
+ })
707
+
708
+
709
+ # ------------------------------------------------- Group K: knowledge --------
710
+ # ClassDB-backed reference/search. (The project-file index — project_search,
711
+ # find_symbol, find_usages, example_snippet — is host-side in tools/knowledge.ts
712
+ # and needs no bridge method.)
713
+
714
+ func _type_name(info: Dictionary) -> String:
715
+ # Readable type for a PropertyInfo/arg dict: the concrete class for objects,
716
+ # else the Variant type name; untyped (TYPE_NIL, no class) reads as "Variant".
717
+ var cn := String(info.get("class_name", ""))
718
+ if cn != "":
719
+ return cn
720
+ var t := int(info.get("type", TYPE_NIL))
721
+ if t == TYPE_NIL:
722
+ return "Variant"
723
+ return type_string(t)
724
+
725
+
726
+ func _doc_url(cls: String) -> String:
727
+ return "https://docs.godotengine.org/en/stable/classes/class_%s.html" % cls.to_lower()
728
+
729
+
730
+ func _doc_member_url(cls: String, kind: String, member: String) -> String:
731
+ # Godot online-docs anchors look like #class-node-method-add-child.
732
+ if member == "":
733
+ return _doc_url(cls)
734
+ var anchor := "class-%s-%s-%s" % [cls.to_lower(), kind, member.replace("_", "-")]
735
+ return "%s#%s" % [_doc_url(cls), anchor]
736
+
737
+
738
+ func _classdb_reference(params: Dictionary) -> Dictionary:
739
+ var cls := String(params.get("class_name", ""))
740
+ if cls == "" or not ClassDB.class_exists(cls):
741
+ return _err("not_found", "Class not found: %s" % cls)
742
+ var inherited := bool(params.get("include_inherited", false))
743
+ var no_inherit := not inherited
744
+ var filter := String(params.get("member", "")).strip_edges()
745
+ var methods: Array = []
746
+ for m in ClassDB.class_get_method_list(cls, no_inherit):
747
+ var nm := String(m.get("name", ""))
748
+ if filter != "" and nm.findn(filter) == -1:
749
+ continue
750
+ var margs: Array = []
751
+ for a in m.get("args", []):
752
+ margs.append({"name": String(a.get("name", "")), "type": _type_name(a)})
753
+ var ret: Dictionary = m.get("return", {})
754
+ var rt := _type_name(ret)
755
+ if String(ret.get("class_name", "")) == "" and int(ret.get("type", TYPE_NIL)) == TYPE_NIL:
756
+ rt = "void"
757
+ methods.append({"name": nm, "return_type": rt, "args": margs})
758
+ var sigs: Array = []
759
+ for s in ClassDB.class_get_signal_list(cls, no_inherit):
760
+ var sn := String(s.get("name", ""))
761
+ if filter != "" and sn.findn(filter) == -1:
762
+ continue
763
+ var sargs: Array = []
764
+ for a in s.get("args", []):
765
+ sargs.append({"name": String(a.get("name", "")), "type": _type_name(a)})
766
+ sigs.append({"name": sn, "args": sargs})
767
+ var props: Array = []
768
+ for p in ClassDB.class_get_property_list(cls, no_inherit):
769
+ var usage := int(p.get("usage", 0))
770
+ if usage & (PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP):
771
+ continue
772
+ var pn := String(p.get("name", ""))
773
+ if pn == "":
774
+ continue
775
+ if filter != "" and pn.findn(filter) == -1:
776
+ continue
777
+ props.append({"name": pn, "type": type_string(int(p.get("type", TYPE_NIL))), "class_name": String(p.get("class_name", ""))})
778
+ return _ok({
779
+ "class": cls,
780
+ "parent": ClassDB.get_parent_class(cls),
781
+ "can_instantiate": ClassDB.can_instantiate(cls),
782
+ "docs_url": _doc_url(cls),
783
+ "methods": methods,
784
+ "signals": sigs,
785
+ "properties": props,
786
+ })
787
+
788
+
789
+ func _docs_scan_members(cls: String, q: String, kind: String, results: Array, limit: int) -> bool:
790
+ # Append member matches for one class; return true once `limit` is reached.
791
+ if kind == "any" or kind == "method":
792
+ for m in ClassDB.class_get_method_list(cls, true):
793
+ var nm := String(m.get("name", ""))
794
+ if nm.to_lower().find(q) != -1:
795
+ results.append({"class": cls, "member": nm, "kind": "method", "docs_url": _doc_member_url(cls, "method", nm)})
796
+ if results.size() >= limit:
797
+ return true
798
+ if kind == "any" or kind == "property":
799
+ for p in ClassDB.class_get_property_list(cls, true):
800
+ var usage := int(p.get("usage", 0))
801
+ if usage & (PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP):
802
+ continue
803
+ var pn := String(p.get("name", ""))
804
+ if pn != "" and pn.to_lower().find(q) != -1:
805
+ results.append({"class": cls, "member": pn, "kind": "property", "docs_url": _doc_member_url(cls, "property", pn)})
806
+ if results.size() >= limit:
807
+ return true
808
+ if kind == "any" or kind == "signal":
809
+ for s in ClassDB.class_get_signal_list(cls, true):
810
+ var sn := String(s.get("name", ""))
811
+ if sn.to_lower().find(q) != -1:
812
+ results.append({"class": cls, "member": sn, "kind": "signal", "docs_url": _doc_member_url(cls, "signal", sn)})
813
+ if results.size() >= limit:
814
+ return true
815
+ return false
816
+
817
+
818
+ func _docs_search(params: Dictionary) -> Dictionary:
819
+ var raw_q := String(params.get("query", ""))
820
+ var q := raw_q.strip_edges().to_lower()
821
+ if q == "":
822
+ return _err("bad_request", "query must not be empty")
823
+ var kind := String(params.get("kind", "any"))
824
+ var scope := String(params.get("class_name", "")).strip_edges()
825
+ var limit := int(params.get("limit", 40))
826
+ if limit <= 0:
827
+ limit = 40
828
+ var deep := bool(params.get("deep", true))
829
+ var results: Array = []
830
+ var truncated := false
831
+
832
+ var classes: Array = Array(ClassDB.get_class_list())
833
+ classes.sort()
834
+
835
+ # Class-name matches (project-wide over the engine class list).
836
+ if kind == "any" or kind == "class":
837
+ for c in classes:
838
+ var cs := String(c)
839
+ if cs.to_lower().find(q) != -1:
840
+ results.append({"class": cs, "member": "", "kind": "class", "docs_url": _doc_url(cs)})
841
+ if results.size() >= limit:
842
+ truncated = true
843
+ break
844
+
845
+ # Member matches (bounded by `limit`).
846
+ if deep and not truncated and kind != "class":
847
+ var scan: Array = []
848
+ if scope != "":
849
+ if ClassDB.class_exists(scope):
850
+ scan = [scope]
851
+ else:
852
+ scan = classes
853
+ for c in scan:
854
+ if _docs_scan_members(String(c), q, kind, results, limit):
855
+ truncated = true
856
+ break
857
+
858
+ return _ok({
859
+ "query": raw_q,
860
+ "count": results.size(),
861
+ "truncated": truncated,
862
+ "results": results,
863
+ })
864
+
865
+
866
+ func _screenshot(params: Dictionary) -> Dictionary:
867
+ var which := String(params.get("viewport", "3d"))
868
+ var vp: SubViewport = null
869
+ if which == "2d":
870
+ vp = EditorInterface.get_editor_viewport_2d()
871
+ else:
872
+ vp = EditorInterface.get_editor_viewport_3d(0)
873
+ if vp == null:
874
+ return _err("no_viewport", "Editor viewport '%s' is not available" % which)
875
+ var tex := vp.get_texture()
876
+ if tex == null:
877
+ return _err("no_texture", "Viewport has no texture yet (open the matching editor tab)")
878
+ var img := tex.get_image()
879
+ if img == null:
880
+ return _err("no_image", "Could not read viewport image")
881
+ var buf := img.save_png_to_buffer()
882
+ return _ok({
883
+ "mime": "image/png",
884
+ "base64": Marshalls.raw_to_base64(buf),
885
+ "width": img.get_width(),
886
+ "height": img.get_height(),
887
+ "viewport": which,
888
+ })
889
+
890
+
891
+ # ------------------------------------------------- Group A: node depth -------
892
+
893
+ func _descendants(node: Node) -> Array:
894
+ var out: Array = []
895
+ for c in node.get_children():
896
+ out.append(c)
897
+ out.append_array(_descendants(c))
898
+ return out
899
+
900
+
901
+ func _node_duplicate(params: Dictionary) -> Dictionary:
902
+ var root := _edited_root()
903
+ if root == null:
904
+ return _err("no_scene", "No scene is open")
905
+ var node := _resolve(root, String(params.get("path", "")))
906
+ if node == null:
907
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
908
+ if node == root:
909
+ return _err("refused", "Cannot duplicate the scene root")
910
+ var parent := node.get_parent()
911
+ var dup: Node = node.duplicate()
912
+ if params.has("name"):
913
+ dup.name = String(params.get("name"))
914
+ var ur := _plugin.get_undo_redo()
915
+ ur.create_action("Breakpoint: duplicate %s" % node.name)
916
+ ur.add_do_method(parent, "add_child", dup)
917
+ ur.add_do_method(dup, "set_owner", root)
918
+ for d in _descendants(dup):
919
+ ur.add_do_method(d, "set_owner", root)
920
+ ur.add_do_reference(dup)
921
+ ur.add_undo_method(parent, "remove_child", dup)
922
+ ur.commit_action()
923
+ return _ok({"path": _path_of(root, dup), "name": String(dup.name), "type": dup.get_class()})
924
+
925
+
926
+ func _node_get_children(params: Dictionary) -> Dictionary:
927
+ var root := _edited_root()
928
+ if root == null:
929
+ return _err("no_scene", "No scene is open")
930
+ var node := _resolve(root, String(params.get("path", "")))
931
+ if node == null:
932
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
933
+ var children: Array = []
934
+ for c in node.get_children():
935
+ children.append({"name": String(c.name), "type": c.get_class(), "path": _path_of(root, c)})
936
+ return _ok({"path": _path_of(root, node), "children": children})
937
+
938
+
939
+ func _node_find(params: Dictionary) -> Dictionary:
940
+ var root := _edited_root()
941
+ if root == null:
942
+ return _err("no_scene", "No scene is open")
943
+ var start := _resolve(root, String(params.get("root_path", ".")))
944
+ if start == null:
945
+ return _err("bad_path", "Search root not found: %s" % params.get("root_path", "."))
946
+ var want_type := String(params.get("type", ""))
947
+ var name_has := String(params.get("name_contains", ""))
948
+ var limit := int(params.get("limit", 200))
949
+ var matches: Array = []
950
+ for n in _descendants(start):
951
+ if want_type != "" and not n.is_class(want_type):
952
+ continue
953
+ if name_has != "" and String(n.name).findn(name_has) == -1:
954
+ continue
955
+ matches.append({"name": String(n.name), "type": n.get_class(), "path": _path_of(root, n)})
956
+ if matches.size() >= limit:
957
+ break
958
+ return _ok({"matches": matches, "count": matches.size()})
959
+
960
+
961
+ func _node_list_groups(params: Dictionary) -> Dictionary:
962
+ var root := _edited_root()
963
+ if root == null:
964
+ return _err("no_scene", "No scene is open")
965
+ var node := _resolve(root, String(params.get("path", "")))
966
+ if node == null:
967
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
968
+ var groups: Array = []
969
+ for g in node.get_groups():
970
+ groups.append(String(g))
971
+ return _ok({"path": _path_of(root, node), "groups": groups})
972
+
973
+
974
+ func _node_add_to_group(params: Dictionary) -> Dictionary:
975
+ var root := _edited_root()
976
+ if root == null:
977
+ return _err("no_scene", "No scene is open")
978
+ var node := _resolve(root, String(params.get("path", "")))
979
+ if node == null:
980
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
981
+ var group := String(params.get("group", ""))
982
+ if group == "":
983
+ return _err("bad_params", "Missing 'group'")
984
+ if node.is_in_group(group):
985
+ return _ok({"path": _path_of(root, node), "group": group, "added": false})
986
+ var ur := _plugin.get_undo_redo()
987
+ ur.create_action("Breakpoint: add %s to group %s" % [node.name, group])
988
+ ur.add_do_method(node, "add_to_group", group, true)
989
+ ur.add_undo_method(node, "remove_from_group", group)
990
+ ur.commit_action()
991
+ return _ok({"path": _path_of(root, node), "group": group, "added": true})
992
+
993
+
994
+ func _node_remove_from_group(params: Dictionary) -> Dictionary:
995
+ var root := _edited_root()
996
+ if root == null:
997
+ return _err("no_scene", "No scene is open")
998
+ var node := _resolve(root, String(params.get("path", "")))
999
+ if node == null:
1000
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1001
+ var group := String(params.get("group", ""))
1002
+ if group == "":
1003
+ return _err("bad_params", "Missing 'group'")
1004
+ if not node.is_in_group(group):
1005
+ return _ok({"path": _path_of(root, node), "group": group, "removed": false})
1006
+ var ur := _plugin.get_undo_redo()
1007
+ ur.create_action("Breakpoint: remove %s from group %s" % [node.name, group])
1008
+ ur.add_do_method(node, "remove_from_group", group)
1009
+ ur.add_undo_method(node, "add_to_group", group, true)
1010
+ ur.commit_action()
1011
+ return _ok({"path": _path_of(root, node), "group": group, "removed": true})
1012
+
1013
+
1014
+ # ------------------------------------- Group A: node depth (batch 2) ---------
1015
+
1016
+ func _node_instantiate_scene(params: Dictionary) -> Dictionary:
1017
+ var root := _edited_root()
1018
+ if root == null:
1019
+ return _err("no_scene", "No scene is open")
1020
+ var parent := _resolve(root, String(params.get("parent_path", "")))
1021
+ if parent == null:
1022
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
1023
+ var scene_path := String(params.get("scene_path", ""))
1024
+ if scene_path == "" or not ResourceLoader.exists(scene_path):
1025
+ return _err("not_found", "Scene not found: %s" % scene_path)
1026
+ var res := ResourceLoader.load(scene_path)
1027
+ if res == null or not (res is PackedScene):
1028
+ return _err("bad_type", "Not a PackedScene: %s" % scene_path)
1029
+ var inst: Node = (res as PackedScene).instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
1030
+ if inst == null:
1031
+ return _err("instantiate_failed", "Could not instantiate %s" % scene_path)
1032
+ if params.has("name"):
1033
+ inst.name = String(params.get("name"))
1034
+ var ur := _plugin.get_undo_redo()
1035
+ ur.create_action("Breakpoint: instance scene %s" % scene_path)
1036
+ ur.add_do_method(parent, "add_child", inst)
1037
+ ur.add_do_method(inst, "set_owner", root)
1038
+ ur.add_do_reference(inst)
1039
+ ur.add_undo_method(parent, "remove_child", inst)
1040
+ ur.commit_action()
1041
+ return _ok({"path": _path_of(root, inst), "name": String(inst.name), "type": inst.get_class(), "scene": scene_path})
1042
+
1043
+
1044
+ func _node_move_child(params: Dictionary) -> Dictionary:
1045
+ var root := _edited_root()
1046
+ if root == null:
1047
+ return _err("no_scene", "No scene is open")
1048
+ var node := _resolve(root, String(params.get("path", "")))
1049
+ if node == null:
1050
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1051
+ if node == root:
1052
+ return _err("refused", "Cannot move the scene root")
1053
+ var parent := node.get_parent()
1054
+ var count := parent.get_child_count()
1055
+ var to_index := int(params.get("to_index", node.get_index()))
1056
+ if to_index < 0:
1057
+ to_index = count + to_index
1058
+ if to_index < 0 or to_index >= count:
1059
+ return _err("bad_index", "to_index out of range 0..%d" % (count - 1))
1060
+ var old_index := node.get_index()
1061
+ var ur := _plugin.get_undo_redo()
1062
+ ur.create_action("Breakpoint: move %s to %d" % [node.name, to_index])
1063
+ ur.add_do_method(parent, "move_child", node, to_index)
1064
+ ur.add_undo_method(parent, "move_child", node, old_index)
1065
+ ur.commit_action()
1066
+ return _ok({"path": _path_of(root, node), "index": node.get_index()})
1067
+
1068
+
1069
+ func _node_change_type(params: Dictionary) -> Dictionary:
1070
+ var root := _edited_root()
1071
+ if root == null:
1072
+ return _err("no_scene", "No scene is open")
1073
+ var node := _resolve(root, String(params.get("path", "")))
1074
+ if node == null:
1075
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1076
+ if node == root:
1077
+ return _err("refused", "Cannot change the type of the scene root")
1078
+ var type := String(params.get("type", ""))
1079
+ if not ClassDB.can_instantiate(type):
1080
+ return _err("bad_type", "Cannot instantiate class: %s" % type)
1081
+ var old_type := node.get_class()
1082
+ var replacement: Node = ClassDB.instantiate(type)
1083
+ replacement.name = node.name
1084
+ var new_props := {}
1085
+ for np in replacement.get_property_list():
1086
+ new_props[String(np.get("name", ""))] = true
1087
+ for op in node.get_property_list():
1088
+ var pname := String(op.get("name", ""))
1089
+ if pname == "" or pname == "name" or pname == "owner" or pname == "script":
1090
+ continue
1091
+ if (int(op.get("usage", 0)) & PROPERTY_USAGE_STORAGE) == 0:
1092
+ continue
1093
+ if not new_props.has(pname):
1094
+ continue
1095
+ replacement.set(pname, node.get(pname))
1096
+ var ur := _plugin.get_undo_redo()
1097
+ ur.create_action("Breakpoint: change type %s -> %s" % [node.name, type])
1098
+ ur.add_do_method(node, "replace_by", replacement, true)
1099
+ ur.add_do_method(replacement, "set_owner", root)
1100
+ ur.add_do_reference(replacement)
1101
+ ur.add_undo_method(replacement, "replace_by", node, true)
1102
+ ur.add_undo_method(node, "set_owner", root)
1103
+ ur.add_undo_reference(node)
1104
+ ur.commit_action()
1105
+ return _ok({"path": _path_of(root, replacement), "name": String(replacement.name), "type": replacement.get_class(), "old_type": old_type})
1106
+
1107
+
1108
+ func _node_set_owner(params: Dictionary) -> Dictionary:
1109
+ var root := _edited_root()
1110
+ if root == null:
1111
+ return _err("no_scene", "No scene is open")
1112
+ var node := _resolve(root, String(params.get("path", "")))
1113
+ if node == null:
1114
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1115
+ if node == root:
1116
+ return _err("refused", "The scene root cannot have an owner")
1117
+ var owner_node: Node = root
1118
+ if params.has("owner_path") and String(params.get("owner_path", "")) != "":
1119
+ owner_node = _resolve(root, String(params.get("owner_path")))
1120
+ if owner_node == null:
1121
+ return _err("bad_path", "Owner not found: %s" % params.get("owner_path", ""))
1122
+ var old_owner := node.owner
1123
+ var ur := _plugin.get_undo_redo()
1124
+ ur.create_action("Breakpoint: set owner of %s" % node.name)
1125
+ ur.add_do_method(node, "set_owner", owner_node)
1126
+ ur.add_undo_method(node, "set_owner", old_owner)
1127
+ ur.commit_action()
1128
+ var owner_out: Variant = (_path_of(root, node.owner) if node.owner else null)
1129
+ return _ok({"path": _path_of(root, node), "owner": owner_out})
1130
+
1131
+
1132
+ func _node_call_method(params: Dictionary) -> Dictionary:
1133
+ var root := _edited_root()
1134
+ if root == null:
1135
+ return _err("no_scene", "No scene is open")
1136
+ var node := _resolve(root, String(params.get("path", "")))
1137
+ if node == null:
1138
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1139
+ var method := String(params.get("method", ""))
1140
+ if method == "":
1141
+ return _err("bad_params", "Missing 'method'")
1142
+ if not node.has_method(method):
1143
+ return _err("no_method", "%s has no method %s" % [node.get_class(), method])
1144
+ var call_args: Array = []
1145
+ for a in params.get("args", []):
1146
+ call_args.append(Codec.decode(a))
1147
+ var result: Variant = node.callv(method, call_args)
1148
+ return _ok({"path": _path_of(root, node), "method": method, "result": Codec.encode(result)})
1149
+
1150
+
1151
+ func _node_get_path(params: Dictionary) -> Dictionary:
1152
+ var root := _edited_root()
1153
+ if root == null:
1154
+ return _err("no_scene", "No scene is open")
1155
+ var node := _resolve(root, String(params.get("path", "")))
1156
+ if node == null:
1157
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1158
+ var parent := node.get_parent()
1159
+ var parent_out: Variant = null
1160
+ if node != root and parent != null:
1161
+ parent_out = _path_of(root, parent)
1162
+ return _ok({
1163
+ "path": _path_of(root, node),
1164
+ "name": String(node.name),
1165
+ "type": node.get_class(),
1166
+ "index": node.get_index(),
1167
+ "parent": parent_out,
1168
+ "child_count": node.get_child_count(),
1169
+ })
1170
+
1171
+
1172
+ func _node_list_properties(params: Dictionary) -> Dictionary:
1173
+ var root := _edited_root()
1174
+ if root == null:
1175
+ return _err("no_scene", "No scene is open")
1176
+ var node := _resolve(root, String(params.get("path", "")))
1177
+ if node == null:
1178
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1179
+ var props: Array = []
1180
+ for p in node.get_property_list():
1181
+ var usage := int(p.get("usage", 0))
1182
+ if (usage & PROPERTY_USAGE_EDITOR) == 0:
1183
+ continue
1184
+ var ptype := int(p.get("type", 0))
1185
+ if ptype == TYPE_NIL:
1186
+ continue
1187
+ props.append({
1188
+ "name": String(p.get("name", "")),
1189
+ "type": ptype,
1190
+ "class_name": String(p.get("class_name", "")),
1191
+ "usage": usage,
1192
+ })
1193
+ return _ok({"path": _path_of(root, node), "properties": props})
1194
+
1195
+
1196
+ # ---------------------------------------------- Group A: scene depth ---------
1197
+
1198
+ func _scene_list_open() -> Dictionary:
1199
+ var scenes: Array = []
1200
+ for p in EditorInterface.get_open_scenes():
1201
+ scenes.append(String(p))
1202
+ var unsaved: Array = []
1203
+ # EditorInterface.get_unsaved_scenes() is Godot 4.4+; a literal call is resolved at PARSE
1204
+ # time and fails to compile the whole addon on 4.3. Guard with has_method + a dynamic
1205
+ # call() defers the lookup to runtime. On <4.4 the unsaved set can't be enumerated, so we
1206
+ # report that via unsaved_supported instead of implying "nothing is unsaved".
1207
+ var unsaved_supported := EditorInterface.has_method("get_unsaved_scenes")
1208
+ if unsaved_supported:
1209
+ for p in EditorInterface.call("get_unsaved_scenes"):
1210
+ unsaved.append(String(p))
1211
+ var root := _edited_root()
1212
+ var current: Variant = null
1213
+ if root and root.scene_file_path != "":
1214
+ current = root.scene_file_path
1215
+ return {"scenes": scenes, "current": current, "unsaved": unsaved, "unsaved_supported": unsaved_supported}
1216
+
1217
+
1218
+ func _scene_reload(params: Dictionary) -> Dictionary:
1219
+ var target := String(params.get("path", ""))
1220
+ if target == "":
1221
+ var root := _edited_root()
1222
+ if root == null:
1223
+ return _err("no_scene", "No scene is open")
1224
+ target = root.scene_file_path
1225
+ if target == "":
1226
+ return _err("bad_params", "Current scene has no saved path yet")
1227
+ if not ResourceLoader.exists(target):
1228
+ return _err("not_found", "Scene not found: %s" % target)
1229
+ EditorInterface.reload_scene_from_path(target)
1230
+ return _ok({"reloaded": target})
1231
+
1232
+
1233
+ func _scene_close(params: Dictionary) -> Dictionary:
1234
+ # EditorInterface.close_scene() is Godot 4.4+; a literal call is resolved at PARSE time and
1235
+ # fails to compile the whole addon on 4.3. Guard with has_method + a dynamic call() defers
1236
+ # the lookup to runtime, so the addon still loads (this tool just reports unsupported).
1237
+ if not EditorInterface.has_method("close_scene"):
1238
+ return _err("unsupported", "scene_close requires Godot 4.4+ (EditorInterface.close_scene is unavailable on this Godot version)")
1239
+ var root := _edited_root()
1240
+ if root == null:
1241
+ return _err("no_scene", "No scene is open")
1242
+ var current := root.scene_file_path
1243
+ var target := String(params.get("path", ""))
1244
+ if target != "" and target != current:
1245
+ return _err("not_current", "Only the current scene (%s) can be closed; open %s first" % [current, target])
1246
+ EditorInterface.call("close_scene")
1247
+ return _ok({"closed": current})
1248
+
1249
+
1250
+ func _scene_get_dependencies(params: Dictionary) -> Dictionary:
1251
+ var target := String(params.get("path", ""))
1252
+ if target == "":
1253
+ var root := _edited_root()
1254
+ if root == null:
1255
+ return _err("no_scene", "No scene is open")
1256
+ target = root.scene_file_path
1257
+ if target == "":
1258
+ return _err("bad_params", "Current scene has no saved path yet")
1259
+ if not ResourceLoader.exists(target):
1260
+ return _err("not_found", "Scene not found: %s" % target)
1261
+ var deps: Array = []
1262
+ for d in ResourceLoader.get_dependencies(target):
1263
+ deps.append(String(d))
1264
+ return _ok({"path": target, "dependencies": deps})
1265
+
1266
+
1267
+ func _scene_pack(params: Dictionary) -> Dictionary:
1268
+ var root := _edited_root()
1269
+ if root == null:
1270
+ return _err("no_scene", "No scene is open")
1271
+ var branch := _resolve(root, String(params.get("path", "")))
1272
+ if branch == null:
1273
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1274
+ var to_path := String(params.get("to_path", ""))
1275
+ if to_path == "" or not to_path.begins_with("res://"):
1276
+ return _err("bad_params", "'to_path' must be a res:// path")
1277
+ var dup: Node = branch.duplicate()
1278
+ if dup == null:
1279
+ return _err("duplicate_failed", "Could not duplicate branch")
1280
+ for d in _descendants(dup):
1281
+ d.owner = dup
1282
+ var packed := PackedScene.new()
1283
+ var e := packed.pack(dup)
1284
+ if e != OK:
1285
+ dup.free()
1286
+ return _err("pack_failed", "PackedScene.pack() returned %d" % e)
1287
+ e = ResourceSaver.save(packed, to_path)
1288
+ dup.free()
1289
+ if e != OK:
1290
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
1291
+ return _ok({"packed": to_path, "branch": _path_of(root, branch)})
1292
+
1293
+
1294
+ func _scene_save_as(params: Dictionary) -> Dictionary:
1295
+ var root := _edited_root()
1296
+ if root == null:
1297
+ return _err("no_scene", "No scene is open")
1298
+ var to_path := String(params.get("path", ""))
1299
+ if to_path == "" or not to_path.begins_with("res://"):
1300
+ return _err("bad_params", "'path' must be a res:// path")
1301
+ EditorInterface.save_scene_as(to_path)
1302
+ return _ok({"saved_as": to_path})
1303
+
1304
+
1305
+ # ---------------------------------------------------- Group A: signals -------
1306
+
1307
+ func _signal_list(params: Dictionary) -> Dictionary:
1308
+ var root := _edited_root()
1309
+ if root == null:
1310
+ return _err("no_scene", "No scene is open")
1311
+ var node := _resolve(root, String(params.get("path", "")))
1312
+ if node == null:
1313
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1314
+ var sigs: Array = []
1315
+ for s in node.get_signal_list():
1316
+ var arg_names: Array = []
1317
+ for a in s.get("args", []):
1318
+ arg_names.append(String(a.get("name", "")))
1319
+ sigs.append({"name": String(s.get("name", "")), "args": arg_names})
1320
+ return _ok({"path": _path_of(root, node), "signals": sigs})
1321
+
1322
+
1323
+ func _signal_list_connections(params: Dictionary) -> Dictionary:
1324
+ var root := _edited_root()
1325
+ if root == null:
1326
+ return _err("no_scene", "No scene is open")
1327
+ var node := _resolve(root, String(params.get("path", "")))
1328
+ if node == null:
1329
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1330
+ var only := String(params.get("signal", ""))
1331
+ var conns: Array = []
1332
+ for s in node.get_signal_list():
1333
+ var sname := String(s.get("name", ""))
1334
+ if only != "" and sname != only:
1335
+ continue
1336
+ for c in node.get_signal_connection_list(sname):
1337
+ var cb: Callable = c.get("callable")
1338
+ var target: Object = cb.get_object()
1339
+ var tpath: Variant = null
1340
+ if target is Node:
1341
+ var tnode := target as Node
1342
+ if tnode == root or root.is_ancestor_of(tnode):
1343
+ tpath = _path_of(root, tnode)
1344
+ else:
1345
+ tpath = String(tnode.name)
1346
+ conns.append({
1347
+ "signal": sname,
1348
+ "target": tpath,
1349
+ "method": String(cb.get_method()),
1350
+ "flags": int(c.get("flags", 0)),
1351
+ })
1352
+ return _ok({"path": _path_of(root, node), "connections": conns})
1353
+
1354
+
1355
+ func _signal_connect(params: Dictionary) -> Dictionary:
1356
+ var root := _edited_root()
1357
+ if root == null:
1358
+ return _err("no_scene", "No scene is open")
1359
+ var node := _resolve(root, String(params.get("path", "")))
1360
+ if node == null:
1361
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1362
+ var sig := String(params.get("signal", ""))
1363
+ if sig == "" or not node.has_signal(sig):
1364
+ return _err("no_signal", "%s has no signal %s" % [node.get_class(), sig])
1365
+ var target := _resolve(root, String(params.get("target_path", "")))
1366
+ if target == null:
1367
+ return _err("bad_path", "Target not found: %s" % params.get("target_path", ""))
1368
+ var method := String(params.get("method", ""))
1369
+ if method == "":
1370
+ return _err("bad_params", "Missing 'method'")
1371
+ if not target.has_method(method):
1372
+ return _err("no_method", "%s has no method %s" % [target.get_class(), method])
1373
+ var flags := int(params.get("flags", 2))
1374
+ var cb := Callable(target, method)
1375
+ if node.is_connected(sig, cb):
1376
+ return _ok({"signal": sig, "source": _path_of(root, node), "target": _path_of(root, target), "method": method, "flags": flags, "connected": false})
1377
+ var ur := _plugin.get_undo_redo()
1378
+ ur.create_action("Breakpoint: connect %s.%s -> %s.%s" % [node.name, sig, target.name, method])
1379
+ ur.add_do_method(node, "connect", sig, cb, flags)
1380
+ ur.add_undo_method(node, "disconnect", sig, cb)
1381
+ ur.commit_action()
1382
+ return _ok({"signal": sig, "source": _path_of(root, node), "target": _path_of(root, target), "method": method, "flags": flags, "connected": true})
1383
+
1384
+
1385
+ func _signal_disconnect(params: Dictionary) -> Dictionary:
1386
+ var root := _edited_root()
1387
+ if root == null:
1388
+ return _err("no_scene", "No scene is open")
1389
+ var node := _resolve(root, String(params.get("path", "")))
1390
+ if node == null:
1391
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1392
+ var sig := String(params.get("signal", ""))
1393
+ if sig == "" or not node.has_signal(sig):
1394
+ return _err("no_signal", "%s has no signal %s" % [node.get_class(), sig])
1395
+ var target := _resolve(root, String(params.get("target_path", "")))
1396
+ if target == null:
1397
+ return _err("bad_path", "Target not found: %s" % params.get("target_path", ""))
1398
+ var method := String(params.get("method", ""))
1399
+ var cb := Callable(target, method)
1400
+ if not node.is_connected(sig, cb):
1401
+ return _ok({"signal": sig, "source": _path_of(root, node), "target": _path_of(root, target), "method": method, "disconnected": false})
1402
+ var flags := 2
1403
+ for c in node.get_signal_connection_list(sig):
1404
+ var ecb: Callable = c.get("callable")
1405
+ if ecb == cb:
1406
+ flags = int(c.get("flags", 2))
1407
+ break
1408
+ var ur := _plugin.get_undo_redo()
1409
+ ur.create_action("Breakpoint: disconnect %s.%s -> %s.%s" % [node.name, sig, target.name, method])
1410
+ ur.add_do_method(node, "disconnect", sig, cb)
1411
+ ur.add_undo_method(node, "connect", sig, cb, flags)
1412
+ ur.commit_action()
1413
+ return _ok({"signal": sig, "source": _path_of(root, node), "target": _path_of(root, target), "method": method, "disconnected": true})
1414
+
1415
+
1416
+ func _signal_add_user_signal(params: Dictionary) -> Dictionary:
1417
+ var root := _edited_root()
1418
+ if root == null:
1419
+ return _err("no_scene", "No scene is open")
1420
+ var node := _resolve(root, String(params.get("path", "")))
1421
+ if node == null:
1422
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1423
+ var sig := String(params.get("signal", ""))
1424
+ if sig == "":
1425
+ return _err("bad_params", "Missing 'signal'")
1426
+ if node.has_signal(sig) or node.has_user_signal(sig):
1427
+ return _err("exists", "Signal already exists: %s" % sig)
1428
+ var arguments: Array = []
1429
+ for a in params.get("args", []):
1430
+ if typeof(a) == TYPE_DICTIONARY:
1431
+ arguments.append({"name": String(a.get("name", "arg")), "type": int(a.get("type", TYPE_NIL))})
1432
+ else:
1433
+ arguments.append({"name": String(a), "type": TYPE_NIL})
1434
+ var ur := _plugin.get_undo_redo()
1435
+ ur.create_action("Breakpoint: add user signal %s.%s" % [node.name, sig])
1436
+ ur.add_do_method(node, "add_user_signal", sig, arguments)
1437
+ ur.add_undo_method(node, "remove_user_signal", sig)
1438
+ ur.commit_action()
1439
+ return _ok({"path": _path_of(root, node), "signal": sig, "added": true})
1440
+
1441
+
1442
+ func _signal_emit(params: Dictionary) -> Dictionary:
1443
+ var root := _edited_root()
1444
+ if root == null:
1445
+ return _err("no_scene", "No scene is open")
1446
+ var node := _resolve(root, String(params.get("path", "")))
1447
+ if node == null:
1448
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1449
+ var sig := String(params.get("signal", ""))
1450
+ if sig == "" or (not node.has_signal(sig) and not node.has_user_signal(sig)):
1451
+ return _err("no_signal", "%s has no signal %s" % [node.get_class(), sig])
1452
+ var call_args: Array = [sig]
1453
+ for a in params.get("args", []):
1454
+ call_args.append(Codec.decode(a))
1455
+ node.callv("emit_signal", call_args)
1456
+ return _ok({"path": _path_of(root, node), "signal": sig, "emitted": true})
1457
+
1458
+
1459
+ # ------------------------------------------------- Group B: resources --------
1460
+
1461
+ func _resource_class_ok(cls: String) -> bool:
1462
+ return ClassDB.class_exists(cls) and ClassDB.is_parent_class(cls, "Resource") and ClassDB.can_instantiate(cls)
1463
+
1464
+
1465
+ func _resource_props(res: Resource) -> Array:
1466
+ var props: Array = []
1467
+ for p in res.get_property_list():
1468
+ var usage := int(p.get("usage", 0))
1469
+ if (usage & PROPERTY_USAGE_EDITOR) == 0:
1470
+ continue
1471
+ var ptype := int(p.get("type", 0))
1472
+ if ptype == TYPE_NIL:
1473
+ continue
1474
+ props.append({
1475
+ "name": String(p.get("name", "")),
1476
+ "type": ptype,
1477
+ "class_name": String(p.get("class_name", "")),
1478
+ "usage": usage,
1479
+ })
1480
+ return props
1481
+
1482
+
1483
+ func _resource_create(params: Dictionary) -> Dictionary:
1484
+ var cls := String(params.get("class_name", ""))
1485
+ if cls == "":
1486
+ return _err("bad_params", "Missing 'class_name'")
1487
+ if not _resource_class_ok(cls):
1488
+ return _err("bad_class", "Not an instantiable Resource class: %s" % cls)
1489
+ var to_path := String(params.get("to_path", ""))
1490
+ if not to_path.begins_with("res://"):
1491
+ return _err("bad_params", "'to_path' must be a res:// path")
1492
+ var res: Resource = ClassDB.instantiate(cls)
1493
+ if res == null:
1494
+ return _err("create_failed", "Could not instantiate %s" % cls)
1495
+ for key in params.get("properties", {}):
1496
+ res.set(String(key), Codec.decode(params["properties"][key]))
1497
+ var e := ResourceSaver.save(res, to_path)
1498
+ if e != OK:
1499
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
1500
+ return _ok({"created": to_path, "type": cls})
1501
+
1502
+
1503
+ func _resource_load(params: Dictionary) -> Dictionary:
1504
+ var path := String(params.get("path", ""))
1505
+ if not ResourceLoader.exists(path):
1506
+ return _err("not_found", "Resource not found: %s" % path)
1507
+ var res := ResourceLoader.load(path)
1508
+ if res == null:
1509
+ return _err("load_failed", "Could not load resource: %s" % path)
1510
+ return _ok({
1511
+ "path": path,
1512
+ "type": res.get_class(),
1513
+ "resource_name": String(res.resource_name),
1514
+ "properties": _resource_props(res),
1515
+ })
1516
+
1517
+
1518
+ func _resource_save(params: Dictionary) -> Dictionary:
1519
+ var from_path := String(params.get("from_path", ""))
1520
+ if not ResourceLoader.exists(from_path):
1521
+ return _err("not_found", "Resource not found: %s" % from_path)
1522
+ var to_path := String(params.get("to_path", from_path))
1523
+ if to_path == "":
1524
+ to_path = from_path
1525
+ if not to_path.begins_with("res://"):
1526
+ return _err("bad_params", "'to_path' must be a res:// path")
1527
+ var res := ResourceLoader.load(from_path)
1528
+ if res == null:
1529
+ return _err("load_failed", "Could not load resource: %s" % from_path)
1530
+ var flags := int(params.get("flags", 0))
1531
+ var e := ResourceSaver.save(res, to_path, flags)
1532
+ if e != OK:
1533
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
1534
+ return _ok({"saved": to_path, "from": from_path})
1535
+
1536
+
1537
+ func _resource_duplicate(params: Dictionary) -> Dictionary:
1538
+ var from_path := String(params.get("path", ""))
1539
+ if not ResourceLoader.exists(from_path):
1540
+ return _err("not_found", "Resource not found: %s" % from_path)
1541
+ var to_path := String(params.get("to_path", ""))
1542
+ if not to_path.begins_with("res://"):
1543
+ return _err("bad_params", "'to_path' must be a res:// path")
1544
+ var res := ResourceLoader.load(from_path)
1545
+ if res == null:
1546
+ return _err("load_failed", "Could not load resource: %s" % from_path)
1547
+ var deep := bool(params.get("deep", false))
1548
+ var dup := res.duplicate(deep)
1549
+ if dup == null:
1550
+ return _err("duplicate_failed", "Could not duplicate resource")
1551
+ var e := ResourceSaver.save(dup, to_path)
1552
+ if e != OK:
1553
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
1554
+ return _ok({"duplicated": to_path, "from": from_path, "deep": deep})
1555
+
1556
+
1557
+ func _resource_get_property(params: Dictionary) -> Dictionary:
1558
+ var path := String(params.get("path", ""))
1559
+ if not ResourceLoader.exists(path):
1560
+ return _err("not_found", "Resource not found: %s" % path)
1561
+ var res := ResourceLoader.load(path)
1562
+ if res == null:
1563
+ return _err("load_failed", "Could not load resource: %s" % path)
1564
+ var prop := String(params.get("property", ""))
1565
+ if prop == "":
1566
+ return _err("bad_params", "Missing 'property'")
1567
+ return _ok({"path": path, "property": prop, "value": Codec.encode(res.get(prop))})
1568
+
1569
+
1570
+ func _resource_set_property(params: Dictionary) -> Dictionary:
1571
+ var path := String(params.get("path", ""))
1572
+ if not ResourceLoader.exists(path):
1573
+ return _err("not_found", "Resource not found: %s" % path)
1574
+ var res := ResourceLoader.load(path)
1575
+ if res == null:
1576
+ return _err("load_failed", "Could not load resource: %s" % path)
1577
+ var prop := String(params.get("property", ""))
1578
+ if prop == "":
1579
+ return _err("bad_params", "Missing 'property'")
1580
+ res.set(prop, Codec.decode(params.get("value")))
1581
+ var e := ResourceSaver.save(res, path)
1582
+ if e != OK:
1583
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
1584
+ return _ok({"path": path, "property": prop, "value": Codec.encode(res.get(prop))})
1585
+
1586
+
1587
+ func _resource_get_import_settings(params: Dictionary) -> Dictionary:
1588
+ var path := String(params.get("path", ""))
1589
+ if path == "":
1590
+ return _err("bad_params", "Missing 'path'")
1591
+ var import_path := path + ".import"
1592
+ if not FileAccess.file_exists(import_path):
1593
+ return _ok({"path": path, "imported": false, "importer": "", "settings": {}})
1594
+ var cfg := ConfigFile.new()
1595
+ var e := cfg.load(import_path)
1596
+ if e != OK:
1597
+ return _err("load_failed", "Could not read import metadata: %s (%d)" % [import_path, e])
1598
+ var importer := ""
1599
+ if cfg.has_section_key("remap", "importer"):
1600
+ importer = String(cfg.get_value("remap", "importer"))
1601
+ var settings: Dictionary = {}
1602
+ if cfg.has_section("params"):
1603
+ for key in cfg.get_section_keys("params"):
1604
+ settings[key] = Codec.encode(cfg.get_value("params", key))
1605
+ return _ok({"path": path, "imported": true, "importer": importer, "settings": settings})
1606
+
1607
+
1608
+ func _resource_set_import_settings(params: Dictionary) -> Dictionary:
1609
+ var path := String(params.get("path", ""))
1610
+ if path == "":
1611
+ return _err("bad_params", "Missing 'path'")
1612
+ var import_path := path + ".import"
1613
+ if not FileAccess.file_exists(import_path):
1614
+ return _err("not_imported", "No .import metadata for %s (not an imported asset)" % path)
1615
+ var cfg := ConfigFile.new()
1616
+ var e := cfg.load(import_path)
1617
+ if e != OK:
1618
+ return _err("load_failed", "Could not read import metadata: %s (%d)" % [import_path, e])
1619
+ var applied: Array = []
1620
+ for key in params.get("settings", {}):
1621
+ cfg.set_value("params", String(key), Codec.decode(params["settings"][key]))
1622
+ applied.append(String(key))
1623
+ e = cfg.save(import_path)
1624
+ if e != OK:
1625
+ return _err("save_failed", "Could not write import metadata (%d)" % e)
1626
+ var reimport := bool(params.get("reimport", true))
1627
+ if reimport:
1628
+ var efs := EditorInterface.get_resource_filesystem()
1629
+ efs.reimport_files(PackedStringArray([path]))
1630
+ return _ok({"path": path, "reimported": reimport, "settings": applied})
1631
+
1632
+
1633
+ # ----------------------------------------------- Group B: filesystem --------
1634
+
1635
+ func _filesystem_list(params: Dictionary) -> Dictionary:
1636
+ var path := String(params.get("path", "res://"))
1637
+ if path == "":
1638
+ path = "res://"
1639
+ var dir := DirAccess.open(path)
1640
+ if dir == null:
1641
+ return _err("not_found", "Directory not found: %s" % path)
1642
+ var dirs: Array = []
1643
+ for d in dir.get_directories():
1644
+ dirs.append(String(d))
1645
+ var files: Array = []
1646
+ for f in dir.get_files():
1647
+ files.append(String(f))
1648
+ return _ok({"path": path, "dirs": dirs, "files": files})
1649
+
1650
+
1651
+ func _filesystem_scan(_params: Dictionary) -> Dictionary:
1652
+ EditorInterface.get_resource_filesystem().scan()
1653
+ return _ok({"scanning": true})
1654
+
1655
+
1656
+ func _filesystem_move(params: Dictionary) -> Dictionary:
1657
+ var from_path := String(params.get("from_path", ""))
1658
+ var to_path := String(params.get("to_path", ""))
1659
+ if not from_path.begins_with("res://") or not to_path.begins_with("res://"):
1660
+ return _err("bad_params", "'from_path' and 'to_path' must be res:// paths")
1661
+ var is_file := FileAccess.file_exists(from_path)
1662
+ var is_dir := DirAccess.dir_exists_absolute(from_path)
1663
+ if not is_file and not is_dir:
1664
+ return _err("not_found", "Source not found: %s" % from_path)
1665
+ if FileAccess.file_exists(to_path) or DirAccess.dir_exists_absolute(to_path):
1666
+ return _err("exists", "Destination already exists: %s" % to_path)
1667
+ var dir := DirAccess.open("res://")
1668
+ if dir == null:
1669
+ return _err("fs_error", "Could not open res://")
1670
+ var e := dir.rename(from_path, to_path)
1671
+ if e != OK:
1672
+ return _err("move_failed", "rename() returned %d" % e)
1673
+ var moved_import := false
1674
+ if is_file and FileAccess.file_exists(from_path + ".import"):
1675
+ dir.rename(from_path + ".import", to_path + ".import")
1676
+ moved_import = true
1677
+ EditorInterface.get_resource_filesystem().scan()
1678
+ return _ok({"moved": to_path, "from": from_path, "moved_import": moved_import})
1679
+
1680
+
1681
+ func _filesystem_create_dir(params: Dictionary) -> Dictionary:
1682
+ var path := String(params.get("path", ""))
1683
+ if not path.begins_with("res://"):
1684
+ return _err("bad_params", "'path' must be a res:// path")
1685
+ if DirAccess.dir_exists_absolute(path):
1686
+ return _ok({"created": path, "existed": true})
1687
+ var e := DirAccess.make_dir_recursive_absolute(path)
1688
+ if e != OK:
1689
+ return _err("mkdir_failed", "make_dir_recursive_absolute() returned %d" % e)
1690
+ EditorInterface.get_resource_filesystem().scan()
1691
+ return _ok({"created": path, "existed": false})
1692
+
1693
+
1694
+ # ---------------------------------------------- Group J: asset generation ----
1695
+ ## The editor-side of Group J. The host owns backend selection + the degrade /
1696
+ ## command-delegation logic; the addon owns two jobs the host cannot do:
1697
+ ## * asset.gen_placeholder — mint a DETERMINISTIC in-engine procedural asset
1698
+ ## (a hashed-colour PNG, an AudioStreamWAV blip, a BoxMesh) and import it.
1699
+ ## * asset.import — register + (re)import a file a configured command backend
1700
+ ## just wrote, and report its imported class.
1701
+ ## No model is ever called here; placeholders are pure functions of the prompt
1702
+ ## hash, so a CI probe can assert them byte-stably.
1703
+
1704
+ func _asset_import(params: Dictionary) -> Dictionary:
1705
+ var path := String(params.get("path", ""))
1706
+ if not path.begins_with("res://"):
1707
+ return _err("bad_params", "'path' must be a res:// path")
1708
+ if not FileAccess.file_exists(path):
1709
+ return _err("not_found", "No file at %s" % path)
1710
+ return _ok(_import_and_describe(path))
1711
+
1712
+
1713
+ ## Make the editor aware of a just-written file, (re)import it when its format
1714
+ ## goes through the import pipeline (image/audio/scene formats), and describe it.
1715
+ ## Native resources (.tres/.res) load directly and are not reimported.
1716
+ func _import_and_describe(path: String) -> Dictionary:
1717
+ var efs := EditorInterface.get_resource_filesystem()
1718
+ efs.update_file(path)
1719
+ var ext := path.get_extension().to_lower()
1720
+ var importable := ["png", "jpg", "jpeg", "webp", "svg", "bmp", "tga", "exr", "hdr",
1721
+ "wav", "ogg", "mp3", "obj", "gltf", "glb", "fbx", "dae"]
1722
+ if importable.has(ext):
1723
+ efs.reimport_files(PackedStringArray([path]))
1724
+ var bytes := 0
1725
+ var f := FileAccess.open(path, FileAccess.READ)
1726
+ if f != null:
1727
+ bytes = int(f.get_length())
1728
+ f.close()
1729
+ var imported_type := ""
1730
+ if ResourceLoader.exists(path):
1731
+ var res := ResourceLoader.load(path)
1732
+ if res != null:
1733
+ imported_type = res.get_class()
1734
+ return {"path": path, "imported_type": imported_type, "bytes": bytes}
1735
+
1736
+
1737
+ func _asset_gen_placeholder(params: Dictionary) -> Dictionary:
1738
+ var kind := String(params.get("kind", ""))
1739
+ var to_path := String(params.get("to_path", ""))
1740
+ if not to_path.begins_with("res://"):
1741
+ return _err("bad_params", "'to_path' must be a res:// path")
1742
+ var prompt := String(params.get("prompt", ""))
1743
+ var seed_str := prompt if prompt != "" else kind
1744
+ var seed_hash: int = abs(hash(seed_str))
1745
+ var base_dir := to_path.get_base_dir()
1746
+ if base_dir != "" and base_dir != "res://" and not DirAccess.dir_exists_absolute(base_dir):
1747
+ DirAccess.make_dir_recursive_absolute(base_dir)
1748
+ match kind:
1749
+ "sprite", "texture", "icon":
1750
+ var w := int(params.get("width", 0))
1751
+ var h := int(params.get("height", 0))
1752
+ if w <= 0:
1753
+ w = 64 if kind == "sprite" else 128
1754
+ if h <= 0:
1755
+ h = w
1756
+ w = clampi(w, 1, 1024)
1757
+ h = clampi(h, 1, 1024)
1758
+ var img := _gen_image(kind, seed_hash, w, h)
1759
+ # Save a NATIVE ImageTexture resource (loads directly, no async import
1760
+ # pipeline) — the placeholder must be usable synchronously. External
1761
+ # formats (a real backend's .png) go through asset.import instead.
1762
+ var tex := ImageTexture.create_from_image(img)
1763
+ var e := ResourceSaver.save(tex, to_path)
1764
+ if e != OK:
1765
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
1766
+ var desc := _import_and_describe(to_path)
1767
+ desc["kind"] = kind
1768
+ desc["width"] = w
1769
+ desc["height"] = h
1770
+ desc["format"] = "ImageTexture"
1771
+ return _ok(desc)
1772
+ "audio_sfx":
1773
+ var dur := clampi(int(params.get("duration_ms", 300)), 20, 5000)
1774
+ var stream := _gen_audio(seed_hash, dur)
1775
+ var e2 := ResourceSaver.save(stream, to_path)
1776
+ if e2 != OK:
1777
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e2)
1778
+ var desc2 := _import_and_describe(to_path)
1779
+ desc2["kind"] = kind
1780
+ desc2["duration_ms"] = dur
1781
+ desc2["format"] = "AudioStreamWAV"
1782
+ return _ok(desc2)
1783
+ "model":
1784
+ var shape := String(params.get("shape", "box"))
1785
+ var mesh := _gen_mesh(shape, seed_hash)
1786
+ var e3 := ResourceSaver.save(mesh, to_path)
1787
+ if e3 != OK:
1788
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e3)
1789
+ var desc3 := _import_and_describe(to_path)
1790
+ desc3["kind"] = kind
1791
+ desc3["shape"] = shape
1792
+ desc3["format"] = mesh.get_class()
1793
+ return _ok(desc3)
1794
+ _:
1795
+ return _err("bad_params", "Unknown kind '%s' (sprite|texture|icon|audio_sfx|model)" % kind)
1796
+
1797
+
1798
+ ## Deterministic procedural image — colours derive from the prompt hash so the
1799
+ ## same prompt always yields the same pixels (CI-assertable).
1800
+ func _gen_image(kind: String, seed_hash: int, w: int, h: int) -> Image:
1801
+ var img := Image.create(w, h, false, Image.FORMAT_RGBA8)
1802
+ var hue := float(seed_hash % 360) / 360.0
1803
+ var base := Color.from_hsv(hue, 0.55, 0.9, 1.0)
1804
+ var accent := Color.from_hsv(fmod(hue + 0.5, 1.0), 0.6, 0.95, 1.0)
1805
+ match kind:
1806
+ "texture":
1807
+ var cell := maxi(4, w / 8)
1808
+ for y in h:
1809
+ for x in w:
1810
+ var checker: Color = base if ((x / cell) + (y / cell)) % 2 == 0 else accent
1811
+ img.set_pixel(x, y, checker)
1812
+ "icon":
1813
+ img.fill(Color(0, 0, 0, 0))
1814
+ var cx := w / 2.0
1815
+ var cy := h / 2.0
1816
+ var rad := minf(cx, cy) - 1.0
1817
+ for y in h:
1818
+ for x in w:
1819
+ var dist := Vector2(x + 0.5 - cx, y + 0.5 - cy).length()
1820
+ if dist <= rad:
1821
+ img.set_pixel(x, y, accent if dist > rad * 0.72 else base)
1822
+ _:
1823
+ img.fill(base)
1824
+ var margin := maxi(1, w / 4)
1825
+ for y in range(margin, h - margin):
1826
+ for x in range(margin, w - margin):
1827
+ img.set_pixel(x, y, accent)
1828
+ return img
1829
+
1830
+
1831
+ ## Deterministic decaying sine blip; frequency derives from the prompt hash.
1832
+ func _gen_audio(seed_hash: int, dur_ms: int) -> AudioStreamWAV:
1833
+ var rate := 22050
1834
+ var n := int(rate * dur_ms / 1000.0)
1835
+ if n < 1:
1836
+ n = 1
1837
+ var freq := 220.0 + float(seed_hash % 660)
1838
+ var data := PackedByteArray()
1839
+ data.resize(n * 2)
1840
+ for i in n:
1841
+ var t := float(i) / float(rate)
1842
+ var env := clampf(1.0 - float(i) / float(n), 0.0, 1.0)
1843
+ var sample := sin(TAU * freq * t) * env * 0.6
1844
+ data.encode_s16(i * 2, int(clampf(sample, -1.0, 1.0) * 32767.0))
1845
+ var stream := AudioStreamWAV.new()
1846
+ stream.format = AudioStreamWAV.FORMAT_16_BITS
1847
+ stream.mix_rate = rate
1848
+ stream.stereo = false
1849
+ stream.data = data
1850
+ return stream
1851
+
1852
+
1853
+ ## Deterministic primitive mesh; size derives from the prompt hash.
1854
+ func _gen_mesh(shape: String, seed_hash: int) -> Mesh:
1855
+ var s := 0.5 + float(seed_hash % 100) / 100.0
1856
+ match shape:
1857
+ "sphere":
1858
+ var sm := SphereMesh.new()
1859
+ sm.radius = s * 0.5
1860
+ sm.height = s
1861
+ return sm
1862
+ "cylinder":
1863
+ var cm := CylinderMesh.new()
1864
+ cm.top_radius = s * 0.5
1865
+ cm.bottom_radius = s * 0.5
1866
+ cm.height = s
1867
+ return cm
1868
+ "prism":
1869
+ var pm := PrismMesh.new()
1870
+ pm.size = Vector3(s, s, s)
1871
+ return pm
1872
+ _:
1873
+ var bm := BoxMesh.new()
1874
+ bm.size = Vector3(s, s, s)
1875
+ return bm
1876
+
1877
+
1878
+ # ------------------------------------------------------ Group C: Animation ----
1879
+ ## Authoring over an in-scene AnimationPlayer. Animations live in the player's
1880
+ ## AnimationLibrary resources; every mutation goes through EditorUndoRedoManager
1881
+ ## (undoable, like the node_* tools), so nothing is written to disk here.
1882
+
1883
+ func _as_anim_player(root: Node, path: String) -> AnimationPlayer:
1884
+ var n := _resolve(root, path)
1885
+ if n is AnimationPlayer:
1886
+ return n
1887
+ return null
1888
+
1889
+
1890
+ func _anim_of(player: AnimationPlayer, lib_name: String, anim_name: String) -> Animation:
1891
+ if not player.has_animation_library(lib_name):
1892
+ return null
1893
+ var lib := player.get_animation_library(lib_name)
1894
+ if lib == null or not lib.has_animation(anim_name):
1895
+ return null
1896
+ return lib.get_animation(anim_name)
1897
+
1898
+
1899
+ func _anim_track_type(s: String) -> int:
1900
+ var m := {
1901
+ "value": Animation.TYPE_VALUE,
1902
+ "position_3d": Animation.TYPE_POSITION_3D,
1903
+ "rotation_3d": Animation.TYPE_ROTATION_3D,
1904
+ "scale_3d": Animation.TYPE_SCALE_3D,
1905
+ "blend_shape": Animation.TYPE_BLEND_SHAPE,
1906
+ "method": Animation.TYPE_METHOD,
1907
+ "bezier": Animation.TYPE_BEZIER,
1908
+ "audio": Animation.TYPE_AUDIO,
1909
+ "animation": Animation.TYPE_ANIMATION,
1910
+ }
1911
+ return int(m.get(s, -1))
1912
+
1913
+
1914
+ func _anim_track_type_name(t: int) -> String:
1915
+ var names := ["value", "position_3d", "rotation_3d", "scale_3d", "blend_shape", "method", "bezier", "audio", "animation"]
1916
+ if t >= 0 and t < names.size():
1917
+ return String(names[t])
1918
+ return "unknown"
1919
+
1920
+
1921
+ func _anim_loop_mode(s: String) -> int:
1922
+ var m := {"none": Animation.LOOP_NONE, "linear": Animation.LOOP_LINEAR, "pingpong": Animation.LOOP_PINGPONG}
1923
+ return int(m.get(s, -1))
1924
+
1925
+
1926
+ func _anim_loop_name(mode: int) -> String:
1927
+ var names := ["none", "linear", "pingpong"]
1928
+ if mode >= 0 and mode < names.size():
1929
+ return String(names[mode])
1930
+ return "unknown"
1931
+
1932
+
1933
+ func _anim_player_create(params: Dictionary) -> Dictionary:
1934
+ var root := _edited_root()
1935
+ if root == null:
1936
+ return _err("no_scene", "No scene is open")
1937
+ var parent := _resolve(root, String(params.get("parent_path", "")))
1938
+ if parent == null:
1939
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
1940
+ var node := AnimationPlayer.new()
1941
+ node.name = String(params.get("name", "AnimationPlayer"))
1942
+ node.add_animation_library("", AnimationLibrary.new())
1943
+ var ur := _plugin.get_undo_redo()
1944
+ ur.create_action("Breakpoint: add AnimationPlayer %s" % node.name)
1945
+ ur.add_do_method(parent, "add_child", node)
1946
+ ur.add_do_method(node, "set_owner", root)
1947
+ ur.add_do_reference(node)
1948
+ ur.add_undo_method(parent, "remove_child", node)
1949
+ ur.commit_action()
1950
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": "AnimationPlayer"})
1951
+
1952
+
1953
+ func _anim_create(params: Dictionary) -> Dictionary:
1954
+ var root := _edited_root()
1955
+ if root == null:
1956
+ return _err("no_scene", "No scene is open")
1957
+ var player := _as_anim_player(root, String(params.get("player_path", "")))
1958
+ if player == null:
1959
+ return _err("bad_path", "AnimationPlayer not found: %s" % params.get("player_path", ""))
1960
+ var lib_name := String(params.get("library", ""))
1961
+ var anim_name := String(params.get("name", ""))
1962
+ if anim_name == "":
1963
+ return _err("bad_params", "Missing 'name'")
1964
+ if player.has_animation_library(lib_name) and player.get_animation_library(lib_name).has_animation(anim_name):
1965
+ return _err("exists", "Animation already exists: %s" % anim_name)
1966
+ var anim := Animation.new()
1967
+ var ur := _plugin.get_undo_redo()
1968
+ ur.create_action("Breakpoint: create animation %s" % anim_name)
1969
+ var lib: AnimationLibrary
1970
+ if player.has_animation_library(lib_name):
1971
+ lib = player.get_animation_library(lib_name)
1972
+ else:
1973
+ lib = AnimationLibrary.new()
1974
+ ur.add_do_method(player, "add_animation_library", lib_name, lib)
1975
+ ur.add_do_reference(lib)
1976
+ ur.add_undo_method(player, "remove_animation_library", lib_name)
1977
+ ur.add_do_method(lib, "add_animation", anim_name, anim)
1978
+ ur.add_do_reference(anim)
1979
+ ur.add_undo_method(lib, "remove_animation", anim_name)
1980
+ ur.commit_action()
1981
+ return _ok({"player": _path_of(root, player), "library": lib_name, "name": anim_name})
1982
+
1983
+
1984
+ func _anim_delete(params: Dictionary) -> Dictionary:
1985
+ var root := _edited_root()
1986
+ if root == null:
1987
+ return _err("no_scene", "No scene is open")
1988
+ var player := _as_anim_player(root, String(params.get("player_path", "")))
1989
+ if player == null:
1990
+ return _err("bad_path", "AnimationPlayer not found: %s" % params.get("player_path", ""))
1991
+ var lib_name := String(params.get("library", ""))
1992
+ var anim_name := String(params.get("name", ""))
1993
+ var anim := _anim_of(player, lib_name, anim_name)
1994
+ if anim == null:
1995
+ return _err("not_found", "Animation not found: %s" % anim_name)
1996
+ var lib := player.get_animation_library(lib_name)
1997
+ var ur := _plugin.get_undo_redo()
1998
+ ur.create_action("Breakpoint: delete animation %s" % anim_name)
1999
+ ur.add_do_method(lib, "remove_animation", anim_name)
2000
+ ur.add_undo_method(lib, "add_animation", anim_name, anim)
2001
+ ur.add_undo_reference(anim)
2002
+ ur.commit_action()
2003
+ return _ok({"player": _path_of(root, player), "library": lib_name, "deleted": anim_name})
2004
+
2005
+
2006
+ func _anim_add_track(params: Dictionary) -> Dictionary:
2007
+ var root := _edited_root()
2008
+ if root == null:
2009
+ return _err("no_scene", "No scene is open")
2010
+ var player := _as_anim_player(root, String(params.get("player_path", "")))
2011
+ if player == null:
2012
+ return _err("bad_path", "AnimationPlayer not found: %s" % params.get("player_path", ""))
2013
+ var anim := _anim_of(player, String(params.get("library", "")), String(params.get("name", "")))
2014
+ if anim == null:
2015
+ return _err("not_found", "Animation not found: %s" % params.get("name", ""))
2016
+ var ttype := _anim_track_type(String(params.get("type", "value")))
2017
+ if ttype < 0:
2018
+ return _err("bad_params", "Unknown track type: %s" % params.get("type", ""))
2019
+ var track_path := String(params.get("path", ""))
2020
+ if track_path == "":
2021
+ return _err("bad_params", "Missing track 'path' (node or node:property the track drives)")
2022
+ var idx := anim.get_track_count()
2023
+ var ur := _plugin.get_undo_redo()
2024
+ ur.create_action("Breakpoint: add %s track" % _anim_track_type_name(ttype))
2025
+ ur.add_do_method(anim, "add_track", ttype, -1)
2026
+ ur.add_do_method(anim, "track_set_path", idx, NodePath(track_path))
2027
+ ur.add_undo_method(anim, "remove_track", idx)
2028
+ ur.commit_action()
2029
+ return _ok({"track": idx, "type": _anim_track_type_name(ttype), "path": track_path})
2030
+
2031
+
2032
+ func _anim_insert_key(params: Dictionary) -> Dictionary:
2033
+ var root := _edited_root()
2034
+ if root == null:
2035
+ return _err("no_scene", "No scene is open")
2036
+ var player := _as_anim_player(root, String(params.get("player_path", "")))
2037
+ if player == null:
2038
+ return _err("bad_path", "AnimationPlayer not found: %s" % params.get("player_path", ""))
2039
+ var anim := _anim_of(player, String(params.get("library", "")), String(params.get("name", "")))
2040
+ if anim == null:
2041
+ return _err("not_found", "Animation not found: %s" % params.get("name", ""))
2042
+ var track := int(params.get("track", -1))
2043
+ if track < 0 or track >= anim.get_track_count():
2044
+ return _err("bad_track", "Track index out of range: %d" % track)
2045
+ if not params.has("value"):
2046
+ return _err("bad_params", "Missing 'value'")
2047
+ var time := float(params.get("time", 0.0))
2048
+ var value: Variant = Codec.decode(params.get("value"))
2049
+ var transition := float(params.get("transition", 1.0))
2050
+ var ur := _plugin.get_undo_redo()
2051
+ ur.create_action("Breakpoint: insert key @ %s" % time)
2052
+ ur.add_do_method(anim, "track_insert_key", track, time, value, transition)
2053
+ ur.add_undo_method(anim, "track_remove_key_at_time", track, time)
2054
+ ur.commit_action()
2055
+ return _ok({"track": track, "time": time, "key_count": anim.track_get_key_count(track)})
2056
+
2057
+
2058
+ func _anim_remove_key(params: Dictionary) -> Dictionary:
2059
+ var root := _edited_root()
2060
+ if root == null:
2061
+ return _err("no_scene", "No scene is open")
2062
+ var player := _as_anim_player(root, String(params.get("player_path", "")))
2063
+ if player == null:
2064
+ return _err("bad_path", "AnimationPlayer not found: %s" % params.get("player_path", ""))
2065
+ var anim := _anim_of(player, String(params.get("library", "")), String(params.get("name", "")))
2066
+ if anim == null:
2067
+ return _err("not_found", "Animation not found: %s" % params.get("name", ""))
2068
+ var track := int(params.get("track", -1))
2069
+ if track < 0 or track >= anim.get_track_count():
2070
+ return _err("bad_track", "Track index out of range: %d" % track)
2071
+ var key := int(params.get("key", -1))
2072
+ if key < 0 or key >= anim.track_get_key_count(track):
2073
+ return _err("bad_key", "Key index out of range: %d" % key)
2074
+ var time := anim.track_get_key_time(track, key)
2075
+ var value: Variant = anim.track_get_key_value(track, key)
2076
+ var transition := anim.track_get_key_transition(track, key)
2077
+ var ur := _plugin.get_undo_redo()
2078
+ ur.create_action("Breakpoint: remove key %d" % key)
2079
+ ur.add_do_method(anim, "track_remove_key", track, key)
2080
+ ur.add_undo_method(anim, "track_insert_key", track, time, value, transition)
2081
+ ur.commit_action()
2082
+ return _ok({"track": track, "removed_key": key, "time": time})
2083
+
2084
+
2085
+ func _anim_set_length(params: Dictionary) -> Dictionary:
2086
+ var root := _edited_root()
2087
+ if root == null:
2088
+ return _err("no_scene", "No scene is open")
2089
+ var player := _as_anim_player(root, String(params.get("player_path", "")))
2090
+ if player == null:
2091
+ return _err("bad_path", "AnimationPlayer not found: %s" % params.get("player_path", ""))
2092
+ var anim := _anim_of(player, String(params.get("library", "")), String(params.get("name", "")))
2093
+ if anim == null:
2094
+ return _err("not_found", "Animation not found: %s" % params.get("name", ""))
2095
+ var length := float(params.get("length", -1.0))
2096
+ if length <= 0.0:
2097
+ return _err("bad_params", "'length' must be greater than 0")
2098
+ var old := anim.length
2099
+ var ur := _plugin.get_undo_redo()
2100
+ ur.create_action("Breakpoint: set animation length")
2101
+ ur.add_do_property(anim, "length", length)
2102
+ ur.add_undo_property(anim, "length", old)
2103
+ ur.commit_action()
2104
+ return _ok({"length": length, "previous": old})
2105
+
2106
+
2107
+ func _anim_set_loop(params: Dictionary) -> Dictionary:
2108
+ var root := _edited_root()
2109
+ if root == null:
2110
+ return _err("no_scene", "No scene is open")
2111
+ var player := _as_anim_player(root, String(params.get("player_path", "")))
2112
+ if player == null:
2113
+ return _err("bad_path", "AnimationPlayer not found: %s" % params.get("player_path", ""))
2114
+ var anim := _anim_of(player, String(params.get("library", "")), String(params.get("name", "")))
2115
+ if anim == null:
2116
+ return _err("not_found", "Animation not found: %s" % params.get("name", ""))
2117
+ var mode := _anim_loop_mode(String(params.get("mode", "")))
2118
+ if mode < 0:
2119
+ return _err("bad_params", "'mode' must be one of: none, linear, pingpong")
2120
+ var old := anim.loop_mode
2121
+ var ur := _plugin.get_undo_redo()
2122
+ ur.create_action("Breakpoint: set animation loop mode")
2123
+ ur.add_do_property(anim, "loop_mode", mode)
2124
+ ur.add_undo_property(anim, "loop_mode", old)
2125
+ ur.commit_action()
2126
+ return _ok({"mode": _anim_loop_name(mode), "previous": _anim_loop_name(old)})
2127
+
2128
+
2129
+ func _anim_get_track_keys(params: Dictionary) -> Dictionary:
2130
+ var root := _edited_root()
2131
+ if root == null:
2132
+ return _err("no_scene", "No scene is open")
2133
+ var player := _as_anim_player(root, String(params.get("player_path", "")))
2134
+ if player == null:
2135
+ return _err("bad_path", "AnimationPlayer not found: %s" % params.get("player_path", ""))
2136
+ var anim := _anim_of(player, String(params.get("library", "")), String(params.get("name", "")))
2137
+ if anim == null:
2138
+ return _err("not_found", "Animation not found: %s" % params.get("name", ""))
2139
+ var track := int(params.get("track", -1))
2140
+ if track < 0 or track >= anim.get_track_count():
2141
+ return _err("bad_track", "Track index out of range: %d" % track)
2142
+ var keys: Array = []
2143
+ for i in anim.track_get_key_count(track):
2144
+ keys.append({
2145
+ "index": i,
2146
+ "time": anim.track_get_key_time(track, i),
2147
+ "value": Codec.encode(anim.track_get_key_value(track, i)),
2148
+ "transition": anim.track_get_key_transition(track, i),
2149
+ })
2150
+ return _ok({"track": track, "type": _anim_track_type_name(anim.track_get_type(track)), "path": String(anim.track_get_path(track)), "keys": keys})
2151
+
2152
+
2153
+ func _anim_list(params: Dictionary) -> Dictionary:
2154
+ var root := _edited_root()
2155
+ if root == null:
2156
+ return _err("no_scene", "No scene is open")
2157
+ var player := _as_anim_player(root, String(params.get("player_path", "")))
2158
+ if player == null:
2159
+ return _err("bad_path", "AnimationPlayer not found: %s" % params.get("player_path", ""))
2160
+ var out: Array = []
2161
+ for lib_name in player.get_animation_library_list():
2162
+ var lib := player.get_animation_library(lib_name)
2163
+ if lib == null:
2164
+ continue
2165
+ for a in lib.get_animation_list():
2166
+ var anim := lib.get_animation(a)
2167
+ var full := String(a)
2168
+ if String(lib_name) != "":
2169
+ full = String(lib_name) + "/" + String(a)
2170
+ out.append({
2171
+ "name": full,
2172
+ "library": String(lib_name),
2173
+ "animation": String(a),
2174
+ "length": anim.length,
2175
+ "loop_mode": _anim_loop_name(anim.loop_mode),
2176
+ "track_count": anim.get_track_count(),
2177
+ })
2178
+ return _ok({"player": _path_of(root, player), "animations": out})
2179
+
2180
+
2181
+ ## AnimationTree authoring (Group C batch 2): create an AnimationTree node and edit
2182
+ ## its tree_root graph (AnimationNodeBlendTree) or state machine (AnimationNodeStateMachine).
2183
+ ## Undoable via EditorUndoRedoManager, ungated (in-scene, like node_* / batch-1 anim_*).
2184
+
2185
+ func _as_anim_tree(root: Node, path: String) -> AnimationTree:
2186
+ var n := _resolve(root, path)
2187
+ if n is AnimationTree:
2188
+ return n
2189
+ return null
2190
+
2191
+
2192
+ func _anim_root_type_class(s: String) -> String:
2193
+ match s:
2194
+ "blend_tree":
2195
+ return "AnimationNodeBlendTree"
2196
+ "state_machine":
2197
+ return "AnimationNodeStateMachine"
2198
+ _:
2199
+ return ""
2200
+
2201
+
2202
+ func _to_vec2(v) -> Vector2:
2203
+ if v is Array and v.size() >= 2:
2204
+ return Vector2(float(v[0]), float(v[1]))
2205
+ return Vector2.ZERO
2206
+
2207
+
2208
+ func _sm_switch_mode(s: String) -> int:
2209
+ var m := {"immediate": 0, "sync": 1, "at_end": 2}
2210
+ return int(m.get(s, -1))
2211
+
2212
+
2213
+ func _sm_switch_name(mode: int) -> String:
2214
+ var names := ["immediate", "sync", "at_end"]
2215
+ if mode >= 0 and mode < names.size():
2216
+ return String(names[mode])
2217
+ return "unknown"
2218
+
2219
+
2220
+ func _sm_advance_mode(s: String) -> int:
2221
+ var m := {"disabled": 0, "enabled": 1, "auto": 2}
2222
+ return int(m.get(s, -1))
2223
+
2224
+
2225
+ func _sm_advance_name(mode: int) -> String:
2226
+ var names := ["disabled", "enabled", "auto"]
2227
+ if mode >= 0 and mode < names.size():
2228
+ return String(names[mode])
2229
+ return "unknown"
2230
+
2231
+
2232
+ ## Resolve the target AnimationNodeStateMachine: tree_root itself when sm_name is
2233
+ ## empty, or a nested state-machine node inside the tree_root graph.
2234
+ func _resolve_state_machine(tree: AnimationTree, sm_name: String):
2235
+ var rootnode = tree.get("tree_root")
2236
+ if rootnode == null:
2237
+ return null
2238
+ if sm_name == "":
2239
+ if rootnode is AnimationNodeStateMachine:
2240
+ return rootnode
2241
+ return null
2242
+ if not rootnode.has_method("has_node") or not rootnode.has_node(sm_name):
2243
+ return null
2244
+ var sub = rootnode.get_node(sm_name)
2245
+ if sub is AnimationNodeStateMachine:
2246
+ return sub
2247
+ return null
2248
+
2249
+
2250
+ func _anim_tree_create(params: Dictionary) -> Dictionary:
2251
+ var root := _edited_root()
2252
+ if root == null:
2253
+ return _err("no_scene", "No scene is open")
2254
+ var parent := _resolve(root, String(params.get("parent_path", "")))
2255
+ if parent == null:
2256
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
2257
+ var rt := String(params.get("root_type", "blend_tree"))
2258
+ var rt_class := _anim_root_type_class(rt)
2259
+ if rt_class == "":
2260
+ return _err("bad_params", "'root_type' must be one of: blend_tree, state_machine")
2261
+ var node := AnimationTree.new()
2262
+ node.name = String(params.get("name", "AnimationTree"))
2263
+ node.set("tree_root", ClassDB.instantiate(rt_class))
2264
+ var anim_player_path := String(params.get("anim_player_path", ""))
2265
+ if anim_player_path != "":
2266
+ node.set("anim_player", NodePath(anim_player_path))
2267
+ node.set("active", bool(params.get("active", false)))
2268
+ var ur := _plugin.get_undo_redo()
2269
+ ur.create_action("Breakpoint: add AnimationTree %s" % node.name)
2270
+ ur.add_do_method(parent, "add_child", node)
2271
+ ur.add_do_method(node, "set_owner", root)
2272
+ ur.add_do_reference(node)
2273
+ ur.add_undo_method(parent, "remove_child", node)
2274
+ ur.commit_action()
2275
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": "AnimationTree", "root_type": rt, "anim_player": anim_player_path, "active": bool(node.get("active"))})
2276
+
2277
+
2278
+ func _anim_tree_add_node(params: Dictionary) -> Dictionary:
2279
+ var root := _edited_root()
2280
+ if root == null:
2281
+ return _err("no_scene", "No scene is open")
2282
+ var tree := _as_anim_tree(root, String(params.get("tree_path", "")))
2283
+ if tree == null:
2284
+ return _err("bad_path", "AnimationTree not found: %s" % params.get("tree_path", ""))
2285
+ var rootnode = tree.get("tree_root")
2286
+ if rootnode == null or not rootnode.has_method("add_node"):
2287
+ return _err("bad_root", "tree_root does not accept nodes (need AnimationNodeBlendTree or AnimationNodeStateMachine)")
2288
+ var node_name := String(params.get("node_name", ""))
2289
+ if node_name == "":
2290
+ return _err("bad_params", "Missing 'node_name'")
2291
+ if rootnode.has_node(node_name):
2292
+ return _err("exists", "Node already exists in the graph: %s" % node_name)
2293
+ var node_type := String(params.get("node_type", ""))
2294
+ if node_type == "" or not ClassDB.can_instantiate(node_type) or not ClassDB.is_parent_class(node_type, "AnimationNode"):
2295
+ return _err("bad_type", "'node_type' must be an instantiable AnimationNode subclass: %s" % node_type)
2296
+ var sub = ClassDB.instantiate(node_type)
2297
+ if params.has("animation") and sub is AnimationNodeAnimation:
2298
+ sub.set("animation", StringName(String(params.get("animation"))))
2299
+ var pos := _to_vec2(params.get("position", null))
2300
+ var ur := _plugin.get_undo_redo()
2301
+ ur.create_action("Breakpoint: add anim node %s" % node_name)
2302
+ ur.add_do_method(rootnode, "add_node", node_name, sub, pos)
2303
+ ur.add_do_reference(sub)
2304
+ ur.add_undo_method(rootnode, "remove_node", node_name)
2305
+ ur.commit_action()
2306
+ return _ok({"tree": _path_of(root, tree), "node_name": node_name, "node_type": node_type, "position": [pos.x, pos.y]})
2307
+
2308
+
2309
+ func _anim_statemachine_add_state(params: Dictionary) -> Dictionary:
2310
+ var root := _edited_root()
2311
+ if root == null:
2312
+ return _err("no_scene", "No scene is open")
2313
+ var tree := _as_anim_tree(root, String(params.get("tree_path", "")))
2314
+ if tree == null:
2315
+ return _err("bad_path", "AnimationTree not found: %s" % params.get("tree_path", ""))
2316
+ var sm_name := String(params.get("state_machine", ""))
2317
+ var sm = _resolve_state_machine(tree, sm_name)
2318
+ if sm == null:
2319
+ return _err("bad_root", "No AnimationNodeStateMachine at %s" % ("tree_root" if sm_name == "" else sm_name))
2320
+ var state_name := String(params.get("state_name", ""))
2321
+ if state_name == "":
2322
+ return _err("bad_params", "Missing 'state_name'")
2323
+ if sm.has_node(state_name):
2324
+ return _err("exists", "State already exists: %s" % state_name)
2325
+ var node_type := String(params.get("node_type", "AnimationNodeAnimation"))
2326
+ if not ClassDB.can_instantiate(node_type) or not ClassDB.is_parent_class(node_type, "AnimationNode"):
2327
+ return _err("bad_type", "'node_type' must be an instantiable AnimationNode subclass: %s" % node_type)
2328
+ var state = ClassDB.instantiate(node_type)
2329
+ var anim_name := String(params.get("animation", ""))
2330
+ if anim_name != "" and state is AnimationNodeAnimation:
2331
+ state.set("animation", StringName(anim_name))
2332
+ var pos := _to_vec2(params.get("position", null))
2333
+ var ur := _plugin.get_undo_redo()
2334
+ ur.create_action("Breakpoint: add state %s" % state_name)
2335
+ ur.add_do_method(sm, "add_node", state_name, state, pos)
2336
+ ur.add_do_reference(state)
2337
+ ur.add_undo_method(sm, "remove_node", state_name)
2338
+ ur.commit_action()
2339
+ return _ok({"tree": _path_of(root, tree), "state_machine": sm_name, "state_name": state_name, "node_type": node_type, "animation": anim_name, "position": [pos.x, pos.y]})
2340
+
2341
+
2342
+ func _anim_statemachine_add_transition(params: Dictionary) -> Dictionary:
2343
+ var root := _edited_root()
2344
+ if root == null:
2345
+ return _err("no_scene", "No scene is open")
2346
+ var tree := _as_anim_tree(root, String(params.get("tree_path", "")))
2347
+ if tree == null:
2348
+ return _err("bad_path", "AnimationTree not found: %s" % params.get("tree_path", ""))
2349
+ var sm_name := String(params.get("state_machine", ""))
2350
+ var sm = _resolve_state_machine(tree, sm_name)
2351
+ if sm == null:
2352
+ return _err("bad_root", "No AnimationNodeStateMachine at %s" % ("tree_root" if sm_name == "" else sm_name))
2353
+ var from_state := String(params.get("from_state", ""))
2354
+ var to_state := String(params.get("to_state", ""))
2355
+ if from_state == "" or to_state == "":
2356
+ return _err("bad_params", "Missing 'from_state' or 'to_state'")
2357
+ if from_state != "Start" and from_state != "End" and not sm.has_node(from_state):
2358
+ return _err("not_found", "from_state not in state machine: %s" % from_state)
2359
+ if to_state != "Start" and to_state != "End" and not sm.has_node(to_state):
2360
+ return _err("not_found", "to_state not in state machine: %s" % to_state)
2361
+ if sm.has_transition(from_state, to_state):
2362
+ return _err("exists", "Transition already exists: %s -> %s" % [from_state, to_state])
2363
+ var switch_s := String(params.get("switch_mode", "immediate"))
2364
+ var switch_mode := _sm_switch_mode(switch_s)
2365
+ if switch_mode < 0:
2366
+ return _err("bad_params", "'switch_mode' must be one of: immediate, sync, at_end")
2367
+ var advance_s := String(params.get("advance_mode", "enabled"))
2368
+ var advance_mode := _sm_advance_mode(advance_s)
2369
+ if advance_mode < 0:
2370
+ return _err("bad_params", "'advance_mode' must be one of: disabled, enabled, auto")
2371
+ var tr := AnimationNodeStateMachineTransition.new()
2372
+ tr.set("xfade_time", float(params.get("xfade_time", 0.0)))
2373
+ tr.set("switch_mode", switch_mode)
2374
+ tr.set("advance_mode", advance_mode)
2375
+ if String(params.get("advance_condition", "")) != "":
2376
+ tr.set("advance_condition", StringName(String(params.get("advance_condition"))))
2377
+ if params.has("priority"):
2378
+ tr.set("priority", int(params.get("priority")))
2379
+ var ur := _plugin.get_undo_redo()
2380
+ ur.create_action("Breakpoint: add transition %s -> %s" % [from_state, to_state])
2381
+ ur.add_do_method(sm, "add_transition", from_state, to_state, tr)
2382
+ ur.add_do_reference(tr)
2383
+ ur.add_undo_method(sm, "remove_transition", from_state, to_state)
2384
+ ur.commit_action()
2385
+ return _ok({"tree": _path_of(root, tree), "state_machine": sm_name, "from_state": from_state, "to_state": to_state, "xfade_time": float(tr.get("xfade_time")), "switch_mode": _sm_switch_name(int(tr.get("switch_mode"))), "advance_mode": _sm_advance_name(int(tr.get("advance_mode"))), "transition_count": sm.get_transition_count()})
2386
+ # --------------------------------------------------------- Group D: tileset --
2387
+ # Disk-backed TileSet authoring: load a .tres TileSet, mutate it, re-save.
2388
+ # All four are file-writing → gated on the host side.
2389
+
2390
+ func _to_vec2i(v) -> Vector2i:
2391
+ if v is Array and v.size() >= 2:
2392
+ return Vector2i(int(v[0]), int(v[1]))
2393
+ return Vector2i.ZERO
2394
+
2395
+
2396
+ func _to_packed_vec2(v) -> PackedVector2Array:
2397
+ var out := PackedVector2Array()
2398
+ if v is Array:
2399
+ for p in v:
2400
+ if p is Array and p.size() >= 2:
2401
+ out.append(Vector2(float(p[0]), float(p[1])))
2402
+ return out
2403
+
2404
+
2405
+ func _load_tileset(path: String):
2406
+ if not ResourceLoader.exists(path):
2407
+ return null
2408
+ var res := ResourceLoader.load(path)
2409
+ if res is TileSet:
2410
+ return res
2411
+ return null
2412
+
2413
+
2414
+ func _tileset_create(params: Dictionary) -> Dictionary:
2415
+ var to_path := String(params.get("to_path", ""))
2416
+ if not to_path.begins_with("res://"):
2417
+ return _err("bad_params", "'to_path' must be a res:// path")
2418
+ var ts := TileSet.new()
2419
+ if params.has("tile_size"):
2420
+ ts.set("tile_size", _to_vec2i(params.get("tile_size")))
2421
+ var e := ResourceSaver.save(ts, to_path)
2422
+ if e != OK:
2423
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
2424
+ var tsz: Vector2i = ts.get("tile_size")
2425
+ return _ok({"created": to_path, "tile_size": [tsz.x, tsz.y]})
2426
+
2427
+
2428
+ func _tileset_add_source(params: Dictionary) -> Dictionary:
2429
+ var tileset_path := String(params.get("tileset_path", ""))
2430
+ var ts = _load_tileset(tileset_path)
2431
+ if ts == null:
2432
+ return _err("not_found", "TileSet not found: %s" % tileset_path)
2433
+ var texture_path := String(params.get("texture_path", ""))
2434
+ if texture_path == "" or not ResourceLoader.exists(texture_path):
2435
+ return _err("not_found", "Texture not found: %s" % texture_path)
2436
+ var tex := ResourceLoader.load(texture_path)
2437
+ if not (tex is Texture2D):
2438
+ return _err("bad_texture", "Not a Texture2D: %s" % texture_path)
2439
+ var atlas := TileSetAtlasSource.new()
2440
+ atlas.set("texture", tex)
2441
+ var region: Vector2i = ts.get("tile_size")
2442
+ if params.has("texture_region_size"):
2443
+ region = _to_vec2i(params.get("texture_region_size"))
2444
+ atlas.set("texture_region_size", region)
2445
+ if params.has("margins"):
2446
+ atlas.set("margins", _to_vec2i(params.get("margins")))
2447
+ if params.has("separation"):
2448
+ atlas.set("separation", _to_vec2i(params.get("separation")))
2449
+ var assigned: int = ts.add_source(atlas, int(params.get("source_id", -1)))
2450
+ var e := ResourceSaver.save(ts, tileset_path)
2451
+ if e != OK:
2452
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
2453
+ return _ok({"tileset": tileset_path, "source_id": assigned, "texture": texture_path, "texture_region_size": [region.x, region.y], "source_count": ts.get_source_count()})
2454
+
2455
+
2456
+ func _tileset_add_tile(params: Dictionary) -> Dictionary:
2457
+ var tileset_path := String(params.get("tileset_path", ""))
2458
+ var ts = _load_tileset(tileset_path)
2459
+ if ts == null:
2460
+ return _err("not_found", "TileSet not found: %s" % tileset_path)
2461
+ var sid := int(params.get("source_id", -1))
2462
+ if not ts.has_source(sid):
2463
+ return _err("not_found", "No source with id %d" % sid)
2464
+ var src = ts.get_source(sid)
2465
+ if not (src is TileSetAtlasSource):
2466
+ return _err("bad_source", "Source %d is not a TileSetAtlasSource" % sid)
2467
+ var coords := _to_vec2i(params.get("atlas_coords"))
2468
+ if src.has_tile(coords):
2469
+ return _err("exists", "Tile already exists at %s" % str(coords))
2470
+ var size := Vector2i(1, 1)
2471
+ if params.has("size"):
2472
+ size = _to_vec2i(params.get("size"))
2473
+ src.create_tile(coords, size)
2474
+ var e := ResourceSaver.save(ts, tileset_path)
2475
+ if e != OK:
2476
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
2477
+ return _ok({"tileset": tileset_path, "source_id": sid, "atlas_coords": [coords.x, coords.y], "size": [size.x, size.y], "tiles_count": src.get_tiles_count()})
2478
+
2479
+
2480
+ func _tileset_set_tile_collision(params: Dictionary) -> Dictionary:
2481
+ var tileset_path := String(params.get("tileset_path", ""))
2482
+ var ts = _load_tileset(tileset_path)
2483
+ if ts == null:
2484
+ return _err("not_found", "TileSet not found: %s" % tileset_path)
2485
+ var sid := int(params.get("source_id", -1))
2486
+ if not ts.has_source(sid):
2487
+ return _err("not_found", "No source with id %d" % sid)
2488
+ var src = ts.get_source(sid)
2489
+ if not (src is TileSetAtlasSource):
2490
+ return _err("bad_source", "Source %d is not a TileSetAtlasSource" % sid)
2491
+ var coords := _to_vec2i(params.get("atlas_coords"))
2492
+ if not src.has_tile(coords):
2493
+ return _err("not_found", "No tile at %s in source %d" % [str(coords), sid])
2494
+ var layer := int(params.get("physics_layer", 0))
2495
+ if layer < 0:
2496
+ return _err("bad_params", "'physics_layer' must be >= 0")
2497
+ var poly := _to_packed_vec2(params.get("polygon"))
2498
+ if poly.size() < 3:
2499
+ return _err("bad_params", "'polygon' needs at least 3 [x, y] points")
2500
+ while ts.get_physics_layers_count() <= layer:
2501
+ ts.add_physics_layer(-1)
2502
+ var td: TileData = src.get_tile_data(coords, 0)
2503
+ if td == null:
2504
+ return _err("no_tile_data", "Could not get TileData for %s" % str(coords))
2505
+ td.add_collision_polygon(layer)
2506
+ var poly_index := td.get_collision_polygons_count(layer) - 1
2507
+ td.set_collision_polygon_points(layer, poly_index, poly)
2508
+ var one_way := bool(params.get("one_way", false))
2509
+ if params.has("one_way"):
2510
+ td.set_collision_polygon_one_way(layer, poly_index, one_way)
2511
+ var e := ResourceSaver.save(ts, tileset_path)
2512
+ if e != OK:
2513
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
2514
+ return _ok({"tileset": tileset_path, "source_id": sid, "atlas_coords": [coords.x, coords.y], "physics_layer": layer, "polygon_index": poly_index, "points": poly.size(), "one_way": one_way})
2515
+
2516
+
2517
+ # ---------------------------------------------- Group D: tilemap (batch 2) --
2518
+ # In-scene TileMapLayer authoring + cell painting. Unlike the disk-backed
2519
+ # tileset_* family (which writes a .tres and is host-gated), these mutate a
2520
+ # TileMapLayer node in the edited scene and are undoable via
2521
+ # EditorUndoRedoManager — the ungated node_* model. Empty cells read back as
2522
+ # source_id -1 / atlas_coords (-1, -1) / alternative 0, which is exactly what
2523
+ # the undo path restores.
2524
+
2525
+ func _as_tilemap_layer(root: Node, path: String) -> TileMapLayer:
2526
+ var n := _resolve(root, path)
2527
+ if n is TileMapLayer:
2528
+ return n
2529
+ return null
2530
+
2531
+
2532
+ func _tilemaplayer_create(params: Dictionary) -> Dictionary:
2533
+ var root := _edited_root()
2534
+ if root == null:
2535
+ return _err("no_scene", "No scene is open")
2536
+ var parent := _resolve(root, String(params.get("parent_path", "")))
2537
+ if parent == null:
2538
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
2539
+ var layer := TileMapLayer.new()
2540
+ layer.name = String(params.get("name", "TileMapLayer"))
2541
+ var tileset_path := String(params.get("tileset_path", ""))
2542
+ if tileset_path != "":
2543
+ var ts = _load_tileset(tileset_path)
2544
+ if ts == null:
2545
+ return _err("not_found", "TileSet not found: %s" % tileset_path)
2546
+ layer.tile_set = ts
2547
+ var ur := _plugin.get_undo_redo()
2548
+ ur.create_action("Breakpoint: add TileMapLayer %s" % layer.name)
2549
+ ur.add_do_method(parent, "add_child", layer)
2550
+ ur.add_do_method(layer, "set_owner", root)
2551
+ ur.add_do_reference(layer)
2552
+ ur.add_undo_method(parent, "remove_child", layer)
2553
+ ur.commit_action()
2554
+ return _ok({"path": _path_of(root, layer), "name": String(layer.name), "type": "TileMapLayer", "tile_set": tileset_path})
2555
+
2556
+
2557
+ func _tilemap_set_cell(params: Dictionary) -> Dictionary:
2558
+ var root := _edited_root()
2559
+ if root == null:
2560
+ return _err("no_scene", "No scene is open")
2561
+ var layer := _as_tilemap_layer(root, String(params.get("path", "")))
2562
+ if layer == null:
2563
+ return _err("bad_path", "TileMapLayer not found: %s" % params.get("path", ""))
2564
+ var coords := _to_vec2i(params.get("coords"))
2565
+ var source_id := int(params.get("source_id", -1))
2566
+ var atlas := Vector2i.ZERO
2567
+ if params.has("atlas_coords"):
2568
+ atlas = _to_vec2i(params.get("atlas_coords"))
2569
+ var alternative := int(params.get("alternative", 0))
2570
+ var old_src := layer.get_cell_source_id(coords)
2571
+ var old_atlas := layer.get_cell_atlas_coords(coords)
2572
+ var old_alt := layer.get_cell_alternative_tile(coords)
2573
+ var ur := _plugin.get_undo_redo()
2574
+ ur.create_action("Breakpoint: set cell %s" % str(coords))
2575
+ ur.add_do_method(layer, "set_cell", coords, source_id, atlas, alternative)
2576
+ ur.add_undo_method(layer, "set_cell", coords, old_src, old_atlas, old_alt)
2577
+ ur.commit_action()
2578
+ return _ok({"path": _path_of(root, layer), "coords": [coords.x, coords.y], "source_id": source_id, "atlas_coords": [atlas.x, atlas.y], "alternative": alternative, "erased": source_id < 0})
2579
+
2580
+
2581
+ func _tilemap_set_cells_rect(params: Dictionary) -> Dictionary:
2582
+ var root := _edited_root()
2583
+ if root == null:
2584
+ return _err("no_scene", "No scene is open")
2585
+ var layer := _as_tilemap_layer(root, String(params.get("path", "")))
2586
+ if layer == null:
2587
+ return _err("bad_path", "TileMapLayer not found: %s" % params.get("path", ""))
2588
+ var r = params.get("rect")
2589
+ if not (r is Array) or r.size() < 4:
2590
+ return _err("bad_params", "'rect' must be [x, y, width, height]")
2591
+ var rx := int(r[0])
2592
+ var ry := int(r[1])
2593
+ var rw := int(r[2])
2594
+ var rh := int(r[3])
2595
+ if rw <= 0 or rh <= 0:
2596
+ return _err("bad_params", "'rect' width and height must be > 0")
2597
+ var area := rw * rh
2598
+ if area > 65536:
2599
+ return _err("too_large", "rect covers %d cells (max 65536); split into multiple calls" % area)
2600
+ var source_id := int(params.get("source_id", -1))
2601
+ var atlas := Vector2i.ZERO
2602
+ if params.has("atlas_coords"):
2603
+ atlas = _to_vec2i(params.get("atlas_coords"))
2604
+ var alternative := int(params.get("alternative", 0))
2605
+ var ur := _plugin.get_undo_redo()
2606
+ ur.create_action("Breakpoint: set cells rect %d,%d %dx%d" % [rx, ry, rw, rh])
2607
+ for dy in range(rh):
2608
+ for dx in range(rw):
2609
+ var c := Vector2i(rx + dx, ry + dy)
2610
+ var os := layer.get_cell_source_id(c)
2611
+ var oa := layer.get_cell_atlas_coords(c)
2612
+ var oal := layer.get_cell_alternative_tile(c)
2613
+ ur.add_do_method(layer, "set_cell", c, source_id, atlas, alternative)
2614
+ ur.add_undo_method(layer, "set_cell", c, os, oa, oal)
2615
+ ur.commit_action()
2616
+ return _ok({"path": _path_of(root, layer), "rect": [rx, ry, rw, rh], "cells": area, "source_id": source_id, "atlas_coords": [atlas.x, atlas.y], "alternative": alternative, "erased": source_id < 0})
2617
+
2618
+
2619
+ func _tilemap_get_cell(params: Dictionary) -> Dictionary:
2620
+ var root := _edited_root()
2621
+ if root == null:
2622
+ return _err("no_scene", "No scene is open")
2623
+ var layer := _as_tilemap_layer(root, String(params.get("path", "")))
2624
+ if layer == null:
2625
+ return _err("bad_path", "TileMapLayer not found: %s" % params.get("path", ""))
2626
+ var coords := _to_vec2i(params.get("coords"))
2627
+ var src := layer.get_cell_source_id(coords)
2628
+ var atlas := layer.get_cell_atlas_coords(coords)
2629
+ var alt := layer.get_cell_alternative_tile(coords)
2630
+ return _ok({"path": _path_of(root, layer), "coords": [coords.x, coords.y], "source_id": src, "atlas_coords": [atlas.x, atlas.y], "alternative": alt, "empty": src < 0})
2631
+
2632
+
2633
+ func _tilemap_clear(params: Dictionary) -> Dictionary:
2634
+ var root := _edited_root()
2635
+ if root == null:
2636
+ return _err("no_scene", "No scene is open")
2637
+ var layer := _as_tilemap_layer(root, String(params.get("path", "")))
2638
+ if layer == null:
2639
+ return _err("bad_path", "TileMapLayer not found: %s" % params.get("path", ""))
2640
+ var used := layer.get_used_cells()
2641
+ var ur := _plugin.get_undo_redo()
2642
+ ur.create_action("Breakpoint: clear TileMapLayer %s" % layer.name)
2643
+ ur.add_do_method(layer, "clear")
2644
+ for c in used:
2645
+ var os := layer.get_cell_source_id(c)
2646
+ var oa := layer.get_cell_atlas_coords(c)
2647
+ var oal := layer.get_cell_alternative_tile(c)
2648
+ ur.add_undo_method(layer, "set_cell", c, os, oa, oal)
2649
+ ur.commit_action()
2650
+ return _ok({"path": _path_of(root, layer), "cleared_cells": used.size()})
2651
+
2652
+
2653
+ # ---------------------------------------------- Group E: physics (batch 1) --
2654
+ # In-scene physics authoring: bodies (Static/Rigid/Character/Area, 2D+3D),
2655
+ # their collision shapes (CollisionShape2D/3D carrying a Shape resource), and
2656
+ # the collision_layer / collision_mask bitmasks. Every mutation goes through
2657
+ # EditorUndoRedoManager and is ungated — the in-scene node_* model.
2658
+
2659
+ func _to_vec3(v) -> Vector3:
2660
+ if v is Array and v.size() >= 3:
2661
+ return Vector3(float(v[0]), float(v[1]), float(v[2]))
2662
+ return Vector3.ZERO
2663
+
2664
+
2665
+ func _to_packed_vec3(v) -> PackedVector3Array:
2666
+ var out := PackedVector3Array()
2667
+ if v is Array:
2668
+ for p in v:
2669
+ if p is Array and p.size() >= 3:
2670
+ out.append(Vector3(float(p[0]), float(p[1]), float(p[2])))
2671
+ return out
2672
+
2673
+
2674
+ func _body_class(kind: String, dim3: bool) -> String:
2675
+ if kind == "static":
2676
+ return "StaticBody3D" if dim3 else "StaticBody2D"
2677
+ if kind == "rigid":
2678
+ return "RigidBody3D" if dim3 else "RigidBody2D"
2679
+ if kind == "character":
2680
+ return "CharacterBody3D" if dim3 else "CharacterBody2D"
2681
+ if kind == "area":
2682
+ return "Area3D" if dim3 else "Area2D"
2683
+ return ""
2684
+
2685
+
2686
+ func _body_create(params: Dictionary) -> Dictionary:
2687
+ var root := _edited_root()
2688
+ if root == null:
2689
+ return _err("no_scene", "No scene is open")
2690
+ var parent := _resolve(root, String(params.get("parent_path", "")))
2691
+ if parent == null:
2692
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
2693
+ var kind := String(params.get("type", ""))
2694
+ var dim3 := String(params.get("dim", "2d")) == "3d"
2695
+ var cls := _body_class(kind, dim3)
2696
+ if cls == "":
2697
+ return _err("bad_params", "Unknown body type '%s' (want static|rigid|character|area)" % kind)
2698
+ if not ClassDB.can_instantiate(cls):
2699
+ return _err("bad_type", "Cannot instantiate class: %s" % cls)
2700
+ var node: Node = ClassDB.instantiate(cls)
2701
+ node.name = String(params.get("name", cls))
2702
+ var ur := _plugin.get_undo_redo()
2703
+ ur.create_action("Breakpoint: add %s" % node.name)
2704
+ ur.add_do_method(parent, "add_child", node)
2705
+ ur.add_do_method(node, "set_owner", root)
2706
+ ur.add_do_reference(node)
2707
+ ur.add_undo_method(parent, "remove_child", node)
2708
+ ur.commit_action()
2709
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": cls, "body": kind, "dim": ("3d" if dim3 else "2d")})
2710
+
2711
+
2712
+ func _make_collision_shape(kind: String, dim3: bool, params: Dictionary):
2713
+ if kind == "rect":
2714
+ if dim3:
2715
+ var b := BoxShape3D.new()
2716
+ b.size = _to_vec3(params.get("size", [1, 1, 1]))
2717
+ return b
2718
+ var r := RectangleShape2D.new()
2719
+ r.size = _to_vec2(params.get("size", [32, 32]))
2720
+ return r
2721
+ if kind == "circle":
2722
+ if dim3:
2723
+ var s := SphereShape3D.new()
2724
+ s.radius = float(params.get("radius", 0.5))
2725
+ return s
2726
+ var c := CircleShape2D.new()
2727
+ c.radius = float(params.get("radius", 16.0))
2728
+ return c
2729
+ if kind == "capsule":
2730
+ if dim3:
2731
+ var cap3 := CapsuleShape3D.new()
2732
+ cap3.radius = float(params.get("radius", 0.5))
2733
+ cap3.height = float(params.get("height", 2.0))
2734
+ return cap3
2735
+ var cap := CapsuleShape2D.new()
2736
+ cap.radius = float(params.get("radius", 16.0))
2737
+ cap.height = float(params.get("height", 48.0))
2738
+ return cap
2739
+ if kind == "polygon":
2740
+ if dim3:
2741
+ var cp3 := ConvexPolygonShape3D.new()
2742
+ cp3.points = _to_packed_vec3(params.get("points"))
2743
+ return cp3
2744
+ var cp := ConvexPolygonShape2D.new()
2745
+ cp.points = _to_packed_vec2(params.get("points"))
2746
+ return cp
2747
+ return null
2748
+
2749
+
2750
+ func _collisionshape_add(params: Dictionary) -> Dictionary:
2751
+ var root := _edited_root()
2752
+ if root == null:
2753
+ return _err("no_scene", "No scene is open")
2754
+ var parent := _resolve(root, String(params.get("parent_path", "")))
2755
+ if parent == null:
2756
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
2757
+ var kind := String(params.get("shape", ""))
2758
+ if not (kind in ["rect", "circle", "capsule", "polygon"]):
2759
+ return _err("bad_params", "Unknown shape '%s' (want rect|circle|capsule|polygon)" % kind)
2760
+ var dim3 := String(params.get("dim", "2d")) == "3d"
2761
+ if kind == "polygon":
2762
+ if dim3 and _to_packed_vec3(params.get("points")).size() < 4:
2763
+ return _err("bad_params", "'polygon' (3D) needs at least 4 points")
2764
+ if not dim3 and _to_packed_vec2(params.get("points")).size() < 3:
2765
+ return _err("bad_params", "'polygon' (2D) needs at least 3 points")
2766
+ var shape_res = _make_collision_shape(kind, dim3, params)
2767
+ if shape_res == null:
2768
+ return _err("bad_params", "Could not build shape for '%s'" % kind)
2769
+ var node: Node
2770
+ if dim3:
2771
+ var cs3 := CollisionShape3D.new()
2772
+ cs3.set("shape", shape_res)
2773
+ node = cs3
2774
+ else:
2775
+ var cs2 := CollisionShape2D.new()
2776
+ cs2.set("shape", shape_res)
2777
+ node = cs2
2778
+ node.name = String(params.get("name", node.get_class()))
2779
+ var ur := _plugin.get_undo_redo()
2780
+ ur.create_action("Breakpoint: add %s" % node.name)
2781
+ ur.add_do_method(parent, "add_child", node)
2782
+ ur.add_do_method(node, "set_owner", root)
2783
+ ur.add_do_reference(node)
2784
+ ur.add_undo_method(parent, "remove_child", node)
2785
+ ur.commit_action()
2786
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": node.get_class(), "shape": kind, "shape_class": shape_res.get_class(), "dim": ("3d" if dim3 else "2d")})
2787
+
2788
+
2789
+ func _body_set_collision_layer(params: Dictionary) -> Dictionary:
2790
+ return _body_set_collision_field(params, "collision_layer", "layer")
2791
+
2792
+
2793
+ func _body_set_collision_mask(params: Dictionary) -> Dictionary:
2794
+ return _body_set_collision_field(params, "collision_mask", "mask")
2795
+
2796
+
2797
+ func _body_set_collision_field(params: Dictionary, field: String, key: String) -> Dictionary:
2798
+ var root := _edited_root()
2799
+ if root == null:
2800
+ return _err("no_scene", "No scene is open")
2801
+ var node := _resolve(root, String(params.get("path", "")))
2802
+ if node == null:
2803
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
2804
+ if not (node is CollisionObject2D or node is CollisionObject3D):
2805
+ return _err("bad_type", "%s is not a physics body/area (CollisionObject2D/3D)" % node.name)
2806
+ if not params.has(key):
2807
+ return _err("bad_params", "Missing '%s'" % key)
2808
+ var new_value := int(params.get(key, 0))
2809
+ if new_value < 0:
2810
+ return _err("bad_params", "'%s' must be a non-negative bitmask" % key)
2811
+ var old_value: int = node.get(field)
2812
+ var ur := _plugin.get_undo_redo()
2813
+ ur.create_action("Breakpoint: set %s.%s" % [node.name, field])
2814
+ ur.add_do_property(node, field, new_value)
2815
+ ur.add_undo_property(node, field, old_value)
2816
+ ur.commit_action()
2817
+ var result := {"path": _path_of(root, node)}
2818
+ result[field] = new_value
2819
+ return _ok(result)
2820
+
2821
+
2822
+ # ---------------------------------------------------------------- Group E batch 2 ----
2823
+ # Physics & collision, part 2: areas (monitoring / gravity zones), joints, collision
2824
+ # polygons, rigidbody tuning, per-body physics material — all in-scene node mutators,
2825
+ # undoable via EditorUndoRedoManager and ungated (the node_* model). _physics_set_gravity
2826
+ # writes ProjectSettings (no undo) and is gated host-side like project_set_setting.
2827
+
2828
+ func _area_set_monitoring(params: Dictionary) -> Dictionary:
2829
+ var root := _edited_root()
2830
+ if root == null:
2831
+ return _err("no_scene", "No scene is open")
2832
+ var node := _resolve(root, String(params.get("path", "")))
2833
+ if node == null:
2834
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
2835
+ if not (node is Area2D or node is Area3D):
2836
+ return _err("bad_type", "%s is not an Area2D/3D" % node.name)
2837
+ if not (params.has("monitoring") or params.has("monitorable")):
2838
+ return _err("bad_params", "Provide 'monitoring' and/or 'monitorable'")
2839
+ var ur := _plugin.get_undo_redo()
2840
+ ur.create_action("Breakpoint: set %s monitoring" % node.name)
2841
+ if params.has("monitoring"):
2842
+ ur.add_do_property(node, "monitoring", bool(params.get("monitoring")))
2843
+ ur.add_undo_property(node, "monitoring", node.get("monitoring"))
2844
+ if params.has("monitorable"):
2845
+ ur.add_do_property(node, "monitorable", bool(params.get("monitorable")))
2846
+ ur.add_undo_property(node, "monitorable", node.get("monitorable"))
2847
+ ur.commit_action()
2848
+ return _ok({"path": _path_of(root, node), "monitoring": node.get("monitoring"), "monitorable": node.get("monitorable")})
2849
+
2850
+
2851
+ func _area_space_override(s: String) -> int:
2852
+ var m := {"disabled": 0, "combine": 1, "combine_replace": 2, "replace": 3, "replace_combine": 4}
2853
+ return int(m.get(s, -1))
2854
+
2855
+
2856
+ func _area_space_override_name(v: int) -> String:
2857
+ var names := ["disabled", "combine", "combine_replace", "replace", "replace_combine"]
2858
+ if v >= 0 and v < names.size():
2859
+ return names[v]
2860
+ return str(v)
2861
+
2862
+
2863
+ func _area_set_gravity(params: Dictionary) -> Dictionary:
2864
+ var root := _edited_root()
2865
+ if root == null:
2866
+ return _err("no_scene", "No scene is open")
2867
+ var node := _resolve(root, String(params.get("path", "")))
2868
+ if node == null:
2869
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
2870
+ if not (node is Area2D or node is Area3D):
2871
+ return _err("bad_type", "%s is not an Area2D/3D" % node.name)
2872
+ if not (params.has("space_override") or params.has("gravity") or params.has("direction") or params.has("point")):
2873
+ return _err("bad_params", "Provide at least one of space_override|gravity|direction|point")
2874
+ var so := -1
2875
+ if params.has("space_override"):
2876
+ so = _area_space_override(String(params.get("space_override")))
2877
+ if so < 0:
2878
+ return _err("bad_params", "Unknown space_override '%s'" % params.get("space_override"))
2879
+ var dim3 := node is Area3D
2880
+ var ur := _plugin.get_undo_redo()
2881
+ ur.create_action("Breakpoint: set %s gravity" % node.name)
2882
+ if params.has("space_override"):
2883
+ ur.add_do_property(node, "gravity_space_override", so)
2884
+ ur.add_undo_property(node, "gravity_space_override", node.get("gravity_space_override"))
2885
+ if params.has("gravity"):
2886
+ ur.add_do_property(node, "gravity", float(params.get("gravity")))
2887
+ ur.add_undo_property(node, "gravity", node.get("gravity"))
2888
+ if params.has("direction"):
2889
+ var dir = _to_vec3(params.get("direction")) if dim3 else _to_vec2(params.get("direction"))
2890
+ ur.add_do_property(node, "gravity_direction", dir)
2891
+ ur.add_undo_property(node, "gravity_direction", node.get("gravity_direction"))
2892
+ if params.has("point"):
2893
+ ur.add_do_property(node, "gravity_point", bool(params.get("point")))
2894
+ ur.add_undo_property(node, "gravity_point", node.get("gravity_point"))
2895
+ ur.commit_action()
2896
+ var dv = node.get("gravity_direction")
2897
+ var dir_out: Array = ([dv.x, dv.y, dv.z] if dim3 else [dv.x, dv.y])
2898
+ return _ok({"path": _path_of(root, node), "space_override": _area_space_override_name(int(node.get("gravity_space_override"))), "gravity": float(node.get("gravity")), "direction": dir_out, "gravity_point": bool(node.get("gravity_point")), "dim": ("3d" if dim3 else "2d")})
2899
+
2900
+
2901
+ func _joint_class(kind: String, dim3: bool) -> String:
2902
+ if dim3:
2903
+ if kind == "pin":
2904
+ return "PinJoint3D"
2905
+ if kind == "hinge":
2906
+ return "HingeJoint3D"
2907
+ if kind == "slider":
2908
+ return "SliderJoint3D"
2909
+ if kind == "cone_twist":
2910
+ return "ConeTwistJoint3D"
2911
+ if kind == "generic6dof":
2912
+ return "Generic6DOFJoint3D"
2913
+ return ""
2914
+ if kind == "pin":
2915
+ return "PinJoint2D"
2916
+ if kind == "groove":
2917
+ return "GrooveJoint2D"
2918
+ if kind == "spring":
2919
+ return "DampedSpringJoint2D"
2920
+ return ""
2921
+
2922
+
2923
+ func _joint_create(params: Dictionary) -> Dictionary:
2924
+ var root := _edited_root()
2925
+ if root == null:
2926
+ return _err("no_scene", "No scene is open")
2927
+ var parent := _resolve(root, String(params.get("parent_path", "")))
2928
+ if parent == null:
2929
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
2930
+ var kind := String(params.get("type", ""))
2931
+ var dim3 := String(params.get("dim", "2d")) == "3d"
2932
+ var cls := _joint_class(kind, dim3)
2933
+ if cls == "":
2934
+ return _err("bad_params", "Unknown joint type '%s' for %s (2D: pin|groove|spring; 3D: pin|hinge|slider|cone_twist|generic6dof)" % [kind, ("3d" if dim3 else "2d")])
2935
+ if not ClassDB.can_instantiate(cls):
2936
+ return _err("bad_type", "Cannot instantiate class: %s" % cls)
2937
+ var node: Node = ClassDB.instantiate(cls)
2938
+ node.name = String(params.get("name", cls))
2939
+ if params.has("node_a"):
2940
+ node.set("node_a", NodePath(String(params.get("node_a"))))
2941
+ if params.has("node_b"):
2942
+ node.set("node_b", NodePath(String(params.get("node_b"))))
2943
+ var ur := _plugin.get_undo_redo()
2944
+ ur.create_action("Breakpoint: add %s" % node.name)
2945
+ ur.add_do_method(parent, "add_child", node)
2946
+ ur.add_do_method(node, "set_owner", root)
2947
+ ur.add_do_reference(node)
2948
+ ur.add_undo_method(parent, "remove_child", node)
2949
+ ur.commit_action()
2950
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": cls, "joint": kind, "dim": ("3d" if dim3 else "2d"), "node_a": String(node.get("node_a")), "node_b": String(node.get("node_b"))})
2951
+
2952
+
2953
+ func _joint_set_bodies(params: Dictionary) -> Dictionary:
2954
+ var root := _edited_root()
2955
+ if root == null:
2956
+ return _err("no_scene", "No scene is open")
2957
+ var node := _resolve(root, String(params.get("path", "")))
2958
+ if node == null:
2959
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
2960
+ if not (node is Joint2D or node is Joint3D):
2961
+ return _err("bad_type", "%s is not a Joint2D/3D" % node.name)
2962
+ if not (params.has("node_a") or params.has("node_b")):
2963
+ return _err("bad_params", "Provide 'node_a' and/or 'node_b'")
2964
+ var ur := _plugin.get_undo_redo()
2965
+ ur.create_action("Breakpoint: set %s bodies" % node.name)
2966
+ if params.has("node_a"):
2967
+ ur.add_do_property(node, "node_a", NodePath(String(params.get("node_a"))))
2968
+ ur.add_undo_property(node, "node_a", node.get("node_a"))
2969
+ if params.has("node_b"):
2970
+ ur.add_do_property(node, "node_b", NodePath(String(params.get("node_b"))))
2971
+ ur.add_undo_property(node, "node_b", node.get("node_b"))
2972
+ ur.commit_action()
2973
+ return _ok({"path": _path_of(root, node), "node_a": String(node.get("node_a")), "node_b": String(node.get("node_b"))})
2974
+
2975
+
2976
+ func _collisionpolygon_add(params: Dictionary) -> Dictionary:
2977
+ var root := _edited_root()
2978
+ if root == null:
2979
+ return _err("no_scene", "No scene is open")
2980
+ var parent := _resolve(root, String(params.get("parent_path", "")))
2981
+ if parent == null:
2982
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
2983
+ var dim3 := String(params.get("dim", "2d")) == "3d"
2984
+ var pts := _to_packed_vec2(params.get("points"))
2985
+ if pts.size() < 3:
2986
+ return _err("bad_params", "collisionpolygon needs at least 3 points")
2987
+ var node: Node
2988
+ if dim3:
2989
+ var c3 := CollisionPolygon3D.new()
2990
+ c3.set("polygon", pts)
2991
+ if params.has("depth"):
2992
+ c3.set("depth", float(params.get("depth")))
2993
+ node = c3
2994
+ else:
2995
+ var c2 := CollisionPolygon2D.new()
2996
+ c2.set("polygon", pts)
2997
+ if params.has("build_mode"):
2998
+ var bm := 1 if String(params.get("build_mode")) == "segments" else 0
2999
+ c2.set("build_mode", bm)
3000
+ node = c2
3001
+ node.name = String(params.get("name", node.get_class()))
3002
+ var ur := _plugin.get_undo_redo()
3003
+ ur.create_action("Breakpoint: add %s" % node.name)
3004
+ ur.add_do_method(parent, "add_child", node)
3005
+ ur.add_do_method(node, "set_owner", root)
3006
+ ur.add_do_reference(node)
3007
+ ur.add_undo_method(parent, "remove_child", node)
3008
+ ur.commit_action()
3009
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": node.get_class(), "dim": ("3d" if dim3 else "2d"), "points": pts.size()})
3010
+
3011
+
3012
+ func _rigidbody_set_properties(params: Dictionary) -> Dictionary:
3013
+ var root := _edited_root()
3014
+ if root == null:
3015
+ return _err("no_scene", "No scene is open")
3016
+ var node := _resolve(root, String(params.get("path", "")))
3017
+ if node == null:
3018
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3019
+ if not (node is RigidBody2D or node is RigidBody3D):
3020
+ return _err("bad_type", "%s is not a RigidBody2D/3D" % node.name)
3021
+ var provided: Array = []
3022
+ for f in ["mass", "gravity_scale", "linear_damp", "angular_damp"]:
3023
+ if params.has(f):
3024
+ provided.append(f)
3025
+ if provided.is_empty():
3026
+ return _err("bad_params", "Provide at least one of mass|gravity_scale|linear_damp|angular_damp")
3027
+ if params.has("mass") and float(params.get("mass")) <= 0.0:
3028
+ return _err("bad_params", "'mass' must be > 0")
3029
+ var ur := _plugin.get_undo_redo()
3030
+ ur.create_action("Breakpoint: set %s physics props" % node.name)
3031
+ for f in provided:
3032
+ ur.add_do_property(node, f, float(params.get(f)))
3033
+ ur.add_undo_property(node, f, node.get(f))
3034
+ ur.commit_action()
3035
+ return _ok({"path": _path_of(root, node), "mass": float(node.get("mass")), "gravity_scale": float(node.get("gravity_scale")), "linear_damp": float(node.get("linear_damp")), "angular_damp": float(node.get("angular_damp"))})
3036
+
3037
+
3038
+ func _body_set_physics_material(params: Dictionary) -> Dictionary:
3039
+ var root := _edited_root()
3040
+ if root == null:
3041
+ return _err("no_scene", "No scene is open")
3042
+ var node := _resolve(root, String(params.get("path", "")))
3043
+ if node == null:
3044
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3045
+ if not (node is StaticBody2D or node is StaticBody3D or node is RigidBody2D or node is RigidBody3D):
3046
+ return _err("bad_type", "%s has no physics_material_override (need StaticBody/RigidBody 2D/3D)" % node.name)
3047
+ var mat := PhysicsMaterial.new()
3048
+ mat.friction = float(params.get("friction", 1.0))
3049
+ mat.bounce = float(params.get("bounce", 0.0))
3050
+ mat.rough = bool(params.get("rough", false))
3051
+ mat.absorbent = bool(params.get("absorbent", false))
3052
+ var ur := _plugin.get_undo_redo()
3053
+ ur.create_action("Breakpoint: set %s physics material" % node.name)
3054
+ ur.add_do_property(node, "physics_material_override", mat)
3055
+ ur.add_undo_property(node, "physics_material_override", node.get("physics_material_override"))
3056
+ ur.add_do_reference(mat)
3057
+ ur.commit_action()
3058
+ return _ok({"path": _path_of(root, node), "friction": mat.friction, "bounce": mat.bounce, "rough": mat.rough, "absorbent": mat.absorbent})
3059
+
3060
+
3061
+ func _physics_set_gravity(params: Dictionary) -> Dictionary:
3062
+ var dim3 := String(params.get("dim", "2d")) == "3d"
3063
+ var prefix := "physics/3d/" if dim3 else "physics/2d/"
3064
+ if not (params.has("magnitude") or params.has("direction")):
3065
+ return _err("bad_params", "Provide 'magnitude' and/or 'direction'")
3066
+ if params.has("magnitude"):
3067
+ ProjectSettings.set_setting(prefix + "default_gravity", float(params.get("magnitude")))
3068
+ if params.has("direction"):
3069
+ var dir = _to_vec3(params.get("direction")) if dim3 else _to_vec2(params.get("direction"))
3070
+ ProjectSettings.set_setting(prefix + "default_gravity_vector", dir)
3071
+ var saved := false
3072
+ if bool(params.get("save", false)):
3073
+ var e := ProjectSettings.save()
3074
+ if e != OK:
3075
+ return _err("save_failed", "ProjectSettings.save() returned %d" % e)
3076
+ saved = true
3077
+ var dv = ProjectSettings.get_setting(prefix + "default_gravity_vector", null)
3078
+ var dir_out: Array = ([dv.x, dv.y, dv.z] if dim3 else [dv.x, dv.y])
3079
+ return _ok({"dim": ("3d" if dim3 else "2d"), "magnitude": float(ProjectSettings.get_setting(prefix + "default_gravity", 0.0)), "direction": dir_out, "saved": saved})
3080
+
3081
+
3082
+ # ---------------------------------------------------------------- Group F batch 1 ----
3083
+ # VFX: GPU particles (2D/3D). All in-scene node/resource mutators, undoable via
3084
+ # EditorUndoRedoManager and ungated (the node_* model). particles_set_texture is
3085
+ # GPUParticles2D-only (3D has no texture; it draws meshes) and feature-detects.
3086
+
3087
+ func _is_particles(node) -> bool:
3088
+ return node is GPUParticles2D or node is GPUParticles3D
3089
+
3090
+
3091
+ func _to_color(v) -> Color:
3092
+ if v is Array and v.size() >= 3:
3093
+ var a: float = float(v[3]) if v.size() >= 4 else 1.0
3094
+ return Color(float(v[0]), float(v[1]), float(v[2]), a)
3095
+ return Color(1, 1, 1, 1)
3096
+
3097
+
3098
+ func _particles_create(params: Dictionary) -> Dictionary:
3099
+ var root := _edited_root()
3100
+ if root == null:
3101
+ return _err("no_scene", "No scene is open")
3102
+ var parent := _resolve(root, String(params.get("parent_path", "")))
3103
+ if parent == null:
3104
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
3105
+ var dim3 := String(params.get("dim", "2d")) == "3d"
3106
+ if params.has("amount") and int(params.get("amount")) <= 0:
3107
+ return _err("bad_params", "'amount' must be > 0")
3108
+ if params.has("lifetime") and float(params.get("lifetime")) <= 0.0:
3109
+ return _err("bad_params", "'lifetime' must be > 0")
3110
+ var node: Node
3111
+ if dim3:
3112
+ node = GPUParticles3D.new()
3113
+ else:
3114
+ node = GPUParticles2D.new()
3115
+ node.name = String(params.get("name", node.get_class()))
3116
+ if params.has("amount"):
3117
+ node.set("amount", int(params.get("amount")))
3118
+ if params.has("lifetime"):
3119
+ node.set("lifetime", float(params.get("lifetime")))
3120
+ if params.has("emitting"):
3121
+ node.set("emitting", bool(params.get("emitting")))
3122
+ var ur := _plugin.get_undo_redo()
3123
+ ur.create_action("Breakpoint: add %s" % node.name)
3124
+ ur.add_do_method(parent, "add_child", node)
3125
+ ur.add_do_method(node, "set_owner", root)
3126
+ ur.add_do_reference(node)
3127
+ ur.add_undo_method(parent, "remove_child", node)
3128
+ ur.commit_action()
3129
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": node.get_class(), "dim": ("3d" if dim3 else "2d"), "amount": int(node.get("amount")), "lifetime": float(node.get("lifetime")), "emitting": bool(node.get("emitting"))})
3130
+
3131
+
3132
+ func _particles_set_process_material(params: Dictionary) -> Dictionary:
3133
+ var root := _edited_root()
3134
+ if root == null:
3135
+ return _err("no_scene", "No scene is open")
3136
+ var node := _resolve(root, String(params.get("path", "")))
3137
+ if node == null:
3138
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3139
+ if not _is_particles(node):
3140
+ return _err("bad_type", "%s is not a GPUParticles2D/3D" % node.name)
3141
+ var mat := ParticleProcessMaterial.new()
3142
+ if params.has("gravity"):
3143
+ mat.gravity = _to_vec3(params.get("gravity"))
3144
+ if params.has("direction"):
3145
+ mat.direction = _to_vec3(params.get("direction"))
3146
+ if params.has("spread"):
3147
+ mat.spread = float(params.get("spread"))
3148
+ if params.has("initial_velocity_min"):
3149
+ mat.initial_velocity_min = float(params.get("initial_velocity_min"))
3150
+ if params.has("initial_velocity_max"):
3151
+ mat.initial_velocity_max = float(params.get("initial_velocity_max"))
3152
+ if params.has("scale_min"):
3153
+ mat.scale_min = float(params.get("scale_min"))
3154
+ if params.has("scale_max"):
3155
+ mat.scale_max = float(params.get("scale_max"))
3156
+ if params.has("color"):
3157
+ mat.color = _to_color(params.get("color"))
3158
+ var ur := _plugin.get_undo_redo()
3159
+ ur.create_action("Breakpoint: set %s process material" % node.name)
3160
+ ur.add_do_property(node, "process_material", mat)
3161
+ ur.add_undo_property(node, "process_material", node.get("process_material"))
3162
+ ur.add_do_reference(mat)
3163
+ ur.commit_action()
3164
+ var g = mat.gravity
3165
+ var d = mat.direction
3166
+ var c = mat.color
3167
+ return _ok({"path": _path_of(root, node), "gravity": [g.x, g.y, g.z], "direction": [d.x, d.y, d.z], "spread": mat.spread, "initial_velocity_min": mat.initial_velocity_min, "initial_velocity_max": mat.initial_velocity_max, "scale_min": mat.scale_min, "scale_max": mat.scale_max, "color": [c.r, c.g, c.b, c.a]})
3168
+
3169
+
3170
+ func _particles_set_amount(params: Dictionary) -> Dictionary:
3171
+ var root := _edited_root()
3172
+ if root == null:
3173
+ return _err("no_scene", "No scene is open")
3174
+ var node := _resolve(root, String(params.get("path", "")))
3175
+ if node == null:
3176
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3177
+ if not _is_particles(node):
3178
+ return _err("bad_type", "%s is not a GPUParticles2D/3D" % node.name)
3179
+ if not params.has("amount"):
3180
+ return _err("bad_params", "Missing 'amount'")
3181
+ var v := int(params.get("amount"))
3182
+ if v <= 0:
3183
+ return _err("bad_params", "'amount' must be > 0")
3184
+ var ur := _plugin.get_undo_redo()
3185
+ ur.create_action("Breakpoint: set %s amount" % node.name)
3186
+ ur.add_do_property(node, "amount", v)
3187
+ ur.add_undo_property(node, "amount", node.get("amount"))
3188
+ ur.commit_action()
3189
+ return _ok({"path": _path_of(root, node), "amount": int(node.get("amount"))})
3190
+
3191
+
3192
+ func _particles_set_lifetime(params: Dictionary) -> Dictionary:
3193
+ var root := _edited_root()
3194
+ if root == null:
3195
+ return _err("no_scene", "No scene is open")
3196
+ var node := _resolve(root, String(params.get("path", "")))
3197
+ if node == null:
3198
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3199
+ if not _is_particles(node):
3200
+ return _err("bad_type", "%s is not a GPUParticles2D/3D" % node.name)
3201
+ if not params.has("lifetime"):
3202
+ return _err("bad_params", "Missing 'lifetime'")
3203
+ var v := float(params.get("lifetime"))
3204
+ if v <= 0.0:
3205
+ return _err("bad_params", "'lifetime' must be > 0")
3206
+ var ur := _plugin.get_undo_redo()
3207
+ ur.create_action("Breakpoint: set %s lifetime" % node.name)
3208
+ ur.add_do_property(node, "lifetime", v)
3209
+ ur.add_undo_property(node, "lifetime", node.get("lifetime"))
3210
+ ur.commit_action()
3211
+ return _ok({"path": _path_of(root, node), "lifetime": float(node.get("lifetime"))})
3212
+
3213
+
3214
+ func _particles_set_emitting(params: Dictionary) -> Dictionary:
3215
+ var root := _edited_root()
3216
+ if root == null:
3217
+ return _err("no_scene", "No scene is open")
3218
+ var node := _resolve(root, String(params.get("path", "")))
3219
+ if node == null:
3220
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3221
+ if not _is_particles(node):
3222
+ return _err("bad_type", "%s is not a GPUParticles2D/3D" % node.name)
3223
+ if not params.has("emitting"):
3224
+ return _err("bad_params", "Missing 'emitting'")
3225
+ var v := bool(params.get("emitting"))
3226
+ var ur := _plugin.get_undo_redo()
3227
+ ur.create_action("Breakpoint: set %s emitting" % node.name)
3228
+ ur.add_do_property(node, "emitting", v)
3229
+ ur.add_undo_property(node, "emitting", node.get("emitting"))
3230
+ ur.commit_action()
3231
+ return _ok({"path": _path_of(root, node), "emitting": bool(node.get("emitting"))})
3232
+
3233
+
3234
+ func _particles_set_texture(params: Dictionary) -> Dictionary:
3235
+ var root := _edited_root()
3236
+ if root == null:
3237
+ return _err("no_scene", "No scene is open")
3238
+ var node := _resolve(root, String(params.get("path", "")))
3239
+ if node == null:
3240
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3241
+ if not (node is GPUParticles2D):
3242
+ return _err("unsupported", "%s has no texture (GPUParticles3D draws meshes; texture is GPUParticles2D-only)" % node.name)
3243
+ var tex_path := String(params.get("texture_path", ""))
3244
+ if tex_path == "" or not ResourceLoader.exists(tex_path):
3245
+ return _err("not_found", "Texture not found: %s" % tex_path)
3246
+ var res = ResourceLoader.load(tex_path)
3247
+ if not (res is Texture2D):
3248
+ return _err("bad_type", "%s is not a Texture2D" % tex_path)
3249
+ var tex := res as Texture2D
3250
+ var ur := _plugin.get_undo_redo()
3251
+ ur.create_action("Breakpoint: set %s texture" % node.name)
3252
+ ur.add_do_property(node, "texture", tex)
3253
+ ur.add_undo_property(node, "texture", node.get("texture"))
3254
+ ur.commit_action()
3255
+ return _ok({"path": _path_of(root, node), "texture_path": tex_path})
3256
+
3257
+
3258
+ # -------------------------------------------------------------- shaders ------
3259
+ # VFX: shaders. shader_create / shader_set_code write .gdshader resources to disk
3260
+ # (host-gated, file-writing). shadermaterial_* mutate the edited scene's node
3261
+ # material (undoable, ungated, node_* model). ShaderMaterial targets
3262
+ # CanvasItem.material (2D / Control) or GeometryInstance3D.material_override (3D);
3263
+ # other nodes degrade to a clear unsupported. Shader / ShaderMaterial /
3264
+ # set_shader_parameter + the shader_parameter/<name> property path probed live on
3265
+ # Godot 4.7.
3266
+
3267
+ func _material_prop(node) -> String:
3268
+ if node is CanvasItem:
3269
+ return "material"
3270
+ if node is GeometryInstance3D:
3271
+ return "material_override"
3272
+ return ""
3273
+
3274
+
3275
+ func _shader_create(params: Dictionary) -> Dictionary:
3276
+ var to_path := String(params.get("to_path", ""))
3277
+ if not to_path.begins_with("res://"):
3278
+ return _err("bad_params", "'to_path' must be a res:// path")
3279
+ var sh := Shader.new()
3280
+ if params.has("code"):
3281
+ sh.code = String(params.get("code"))
3282
+ var e := ResourceSaver.save(sh, to_path)
3283
+ if e != OK:
3284
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
3285
+ return _ok({"created": to_path, "type": "Shader", "code_length": sh.code.length()})
3286
+
3287
+
3288
+ func _shader_set_code(params: Dictionary) -> Dictionary:
3289
+ var path := String(params.get("path", ""))
3290
+ if not ResourceLoader.exists(path):
3291
+ return _err("not_found", "Shader not found: %s" % path)
3292
+ if not params.has("code"):
3293
+ return _err("bad_params", "Missing 'code'")
3294
+ var res = ResourceLoader.load(path)
3295
+ if not (res is Shader):
3296
+ return _err("bad_type", "%s is not a Shader" % path)
3297
+ var sh := res as Shader
3298
+ sh.code = String(params.get("code"))
3299
+ var e := ResourceSaver.save(sh, path)
3300
+ if e != OK:
3301
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
3302
+ return _ok({"path": path, "code_length": sh.code.length()})
3303
+
3304
+
3305
+ func _shadermaterial_create(params: Dictionary) -> Dictionary:
3306
+ var root := _edited_root()
3307
+ if root == null:
3308
+ return _err("no_scene", "No scene is open")
3309
+ var node := _resolve(root, String(params.get("path", "")))
3310
+ if node == null:
3311
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3312
+ var prop := _material_prop(node)
3313
+ if prop == "":
3314
+ return _err("unsupported", "%s has no material slot (needs CanvasItem.material or GeometryInstance3D.material_override)" % node.name)
3315
+ var mat := ShaderMaterial.new()
3316
+ var shader_path := String(params.get("shader_path", ""))
3317
+ if shader_path != "":
3318
+ if not ResourceLoader.exists(shader_path):
3319
+ return _err("not_found", "Shader not found: %s" % shader_path)
3320
+ var sres = ResourceLoader.load(shader_path)
3321
+ if not (sres is Shader):
3322
+ return _err("bad_type", "%s is not a Shader" % shader_path)
3323
+ mat.shader = sres as Shader
3324
+ var ur := _plugin.get_undo_redo()
3325
+ ur.create_action("Breakpoint: set %s shader material" % node.name)
3326
+ ur.add_do_property(node, prop, mat)
3327
+ ur.add_undo_property(node, prop, node.get(prop))
3328
+ ur.add_do_reference(mat)
3329
+ ur.commit_action()
3330
+ return _ok({"path": _path_of(root, node), "target_property": prop, "type": node.get_class(), "shader_path": shader_path})
3331
+
3332
+
3333
+ func _shadermaterial_set_shader(params: Dictionary) -> Dictionary:
3334
+ var root := _edited_root()
3335
+ if root == null:
3336
+ return _err("no_scene", "No scene is open")
3337
+ var node := _resolve(root, String(params.get("path", "")))
3338
+ if node == null:
3339
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3340
+ var prop := _material_prop(node)
3341
+ if prop == "":
3342
+ return _err("unsupported", "%s has no material slot" % node.name)
3343
+ var cur = node.get(prop)
3344
+ if not (cur is ShaderMaterial):
3345
+ return _err("bad_type", "%s has no ShaderMaterial (create one first)" % node.name)
3346
+ var shader_path := String(params.get("shader_path", ""))
3347
+ if not ResourceLoader.exists(shader_path):
3348
+ return _err("not_found", "Shader not found: %s" % shader_path)
3349
+ var sres = ResourceLoader.load(shader_path)
3350
+ if not (sres is Shader):
3351
+ return _err("bad_type", "%s is not a Shader" % shader_path)
3352
+ var mat := cur as ShaderMaterial
3353
+ var ur := _plugin.get_undo_redo()
3354
+ ur.create_action("Breakpoint: set %s shader" % node.name)
3355
+ ur.add_do_property(mat, "shader", sres as Shader)
3356
+ ur.add_undo_property(mat, "shader", mat.shader)
3357
+ ur.commit_action()
3358
+ return _ok({"path": _path_of(root, node), "shader_path": shader_path})
3359
+
3360
+
3361
+ func _shadermaterial_set_param(params: Dictionary) -> Dictionary:
3362
+ var root := _edited_root()
3363
+ if root == null:
3364
+ return _err("no_scene", "No scene is open")
3365
+ var node := _resolve(root, String(params.get("path", "")))
3366
+ if node == null:
3367
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3368
+ var prop := _material_prop(node)
3369
+ if prop == "":
3370
+ return _err("unsupported", "%s has no material slot" % node.name)
3371
+ var cur = node.get(prop)
3372
+ if not (cur is ShaderMaterial):
3373
+ return _err("bad_type", "%s has no ShaderMaterial (create one first)" % node.name)
3374
+ var pname := String(params.get("param", ""))
3375
+ if pname == "":
3376
+ return _err("bad_params", "Missing 'param'")
3377
+ var mat := cur as ShaderMaterial
3378
+ var value: Variant = Codec.decode(params.get("value"))
3379
+ var key := "shader_parameter/" + pname
3380
+ var old_value = mat.get(key)
3381
+ var ur := _plugin.get_undo_redo()
3382
+ ur.create_action("Breakpoint: set %s shader param %s" % [node.name, pname])
3383
+ ur.add_do_property(mat, key, value)
3384
+ ur.add_undo_property(mat, key, old_value)
3385
+ ur.commit_action()
3386
+ return _ok({"path": _path_of(root, node), "param": pname, "value": Codec.encode(mat.get(key))})
3387
+
3388
+ # ---------------------------------------------------------------- Group F batch 3 ----
3389
+ # Audio. Two models. audio_player_create / audio_set_stream mutate the edited scene
3390
+ # (AudioStreamPlayer / AudioStreamPlayer2D / AudioStreamPlayer3D), undoable via
3391
+ # EditorUndoRedoManager and ungated (the node_* model). The four bus tools drive the
3392
+ # global AudioServer (project-wide, not scene-undoable) and are gated host-side like
3393
+ # physics_set_gravity; audio_set_bus_layout writes a .tres, a file-writer too.
3394
+ # AudioServer bus API + player stream/autoplay/volume_db/bus props probed live on Godot 4.7.
3395
+
3396
+ func _is_audio_player(node) -> bool:
3397
+ return node is AudioStreamPlayer or node is AudioStreamPlayer2D or node is AudioStreamPlayer3D
3398
+
3399
+
3400
+ func _audio_player_create(params: Dictionary) -> Dictionary:
3401
+ var root := _edited_root()
3402
+ if root == null:
3403
+ return _err("no_scene", "No scene is open")
3404
+ var parent := _resolve(root, String(params.get("parent_path", "")))
3405
+ if parent == null:
3406
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
3407
+ var dim := String(params.get("dim", "none"))
3408
+ var node: Node
3409
+ if dim == "2d":
3410
+ node = AudioStreamPlayer2D.new()
3411
+ elif dim == "3d":
3412
+ node = AudioStreamPlayer3D.new()
3413
+ else:
3414
+ node = AudioStreamPlayer.new()
3415
+ node.name = String(params.get("name", node.get_class()))
3416
+ if params.has("autoplay"):
3417
+ node.set("autoplay", bool(params.get("autoplay")))
3418
+ if params.has("volume_db"):
3419
+ node.set("volume_db", float(params.get("volume_db")))
3420
+ if params.has("bus"):
3421
+ node.set("bus", String(params.get("bus")))
3422
+ var stream_path := String(params.get("stream_path", ""))
3423
+ if stream_path != "":
3424
+ if not ResourceLoader.exists(stream_path):
3425
+ return _err("not_found", "Stream not found: %s" % stream_path)
3426
+ var sres = ResourceLoader.load(stream_path)
3427
+ if not (sres is AudioStream):
3428
+ return _err("bad_type", "%s is not an AudioStream" % stream_path)
3429
+ node.set("stream", sres as AudioStream)
3430
+ var ur := _plugin.get_undo_redo()
3431
+ ur.create_action("Breakpoint: add %s" % node.name)
3432
+ ur.add_do_method(parent, "add_child", node)
3433
+ ur.add_do_method(node, "set_owner", root)
3434
+ ur.add_do_reference(node)
3435
+ ur.add_undo_method(parent, "remove_child", node)
3436
+ ur.commit_action()
3437
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": node.get_class(), "dim": dim, "autoplay": bool(node.get("autoplay")), "volume_db": float(node.get("volume_db")), "bus": String(node.get("bus")), "stream_path": stream_path})
3438
+
3439
+
3440
+ func _audio_set_stream(params: Dictionary) -> Dictionary:
3441
+ var root := _edited_root()
3442
+ if root == null:
3443
+ return _err("no_scene", "No scene is open")
3444
+ var node := _resolve(root, String(params.get("path", "")))
3445
+ if node == null:
3446
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3447
+ if not _is_audio_player(node):
3448
+ return _err("bad_type", "%s is not an AudioStreamPlayer/2D/3D" % node.name)
3449
+ var stream_path := String(params.get("stream_path", ""))
3450
+ if stream_path == "" or not ResourceLoader.exists(stream_path):
3451
+ return _err("not_found", "Stream not found: %s" % stream_path)
3452
+ var res = ResourceLoader.load(stream_path)
3453
+ if not (res is AudioStream):
3454
+ return _err("bad_type", "%s is not an AudioStream" % stream_path)
3455
+ var stream := res as AudioStream
3456
+ var ur := _plugin.get_undo_redo()
3457
+ ur.create_action("Breakpoint: set %s stream" % node.name)
3458
+ ur.add_do_property(node, "stream", stream)
3459
+ ur.add_undo_property(node, "stream", node.get("stream"))
3460
+ ur.commit_action()
3461
+ return _ok({"path": _path_of(root, node), "stream_path": stream_path})
3462
+
3463
+
3464
+ func _audio_bus_add(params: Dictionary) -> Dictionary:
3465
+ var at := int(params.get("at_position", -1))
3466
+ AudioServer.add_bus(at)
3467
+ var count := AudioServer.get_bus_count()
3468
+ var idx: int = (count - 1) if at < 0 or at >= count else at
3469
+ if params.has("name"):
3470
+ AudioServer.set_bus_name(idx, String(params.get("name")))
3471
+ if params.has("send"):
3472
+ AudioServer.set_bus_send(idx, String(params.get("send")))
3473
+ return _ok({"index": idx, "name": String(AudioServer.get_bus_name(idx)), "send": String(AudioServer.get_bus_send(idx)), "count": AudioServer.get_bus_count()})
3474
+
3475
+
3476
+ func _audio_bus_add_effect(params: Dictionary) -> Dictionary:
3477
+ var bus := String(params.get("bus", ""))
3478
+ var idx: int = AudioServer.get_bus_index(bus)
3479
+ if idx < 0:
3480
+ return _err("not_found", "Bus not found: %s" % bus)
3481
+ var cls := String(params.get("effect", ""))
3482
+ if cls == "" or not ClassDB.can_instantiate(cls) or not ClassDB.is_parent_class(cls, "AudioEffect"):
3483
+ return _err("bad_params", "'effect' must be an instantiable AudioEffect class: %s" % cls)
3484
+ var fx = ClassDB.instantiate(cls)
3485
+ if not (fx is AudioEffect):
3486
+ return _err("bad_type", "%s is not an AudioEffect" % cls)
3487
+ var at := int(params.get("at_position", -1))
3488
+ AudioServer.add_bus_effect(idx, fx, at)
3489
+ return _ok({"bus": bus, "bus_index": idx, "effect": cls, "effect_count": AudioServer.get_bus_effect_count(idx)})
3490
+
3491
+
3492
+ func _audio_bus_set_volume(params: Dictionary) -> Dictionary:
3493
+ var bus := String(params.get("bus", ""))
3494
+ var idx: int = AudioServer.get_bus_index(bus)
3495
+ if idx < 0:
3496
+ return _err("not_found", "Bus not found: %s" % bus)
3497
+ if not params.has("volume_db"):
3498
+ return _err("bad_params", "Missing 'volume_db'")
3499
+ AudioServer.set_bus_volume_db(idx, float(params.get("volume_db")))
3500
+ return _ok({"bus": bus, "bus_index": idx, "volume_db": AudioServer.get_bus_volume_db(idx)})
3501
+
3502
+
3503
+ func _audio_set_bus_layout(params: Dictionary) -> Dictionary:
3504
+ var to_path := String(params.get("to_path", "res://default_bus_layout.tres"))
3505
+ if not to_path.begins_with("res://"):
3506
+ return _err("bad_params", "'to_path' must be a res:// path")
3507
+ var layout = AudioServer.generate_bus_layout()
3508
+ var e := ResourceSaver.save(layout, to_path)
3509
+ if e != OK:
3510
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
3511
+ return _ok({"saved": to_path, "bus_count": AudioServer.get_bus_count()})
3512
+
3513
+
3514
+ # ---------------------------------------------------------------- Group G: UI / control / theming ----
3515
+ # control_* + container_add_child mutate the edited scene (Control nodes), undoable via
3516
+ # EditorUndoRedoManager and ungated (the node_* model). theme_* author a Theme resource (or its
3517
+ # entries) on disk via ResourceSaver and are host-gated file-writers like resource_* / shader_create.
3518
+ # Control anchors / set_anchors_and_offsets_preset / size flags / theme and Theme.set_color /
3519
+ # set_font / set_stylebox / set_constant were probed live on Godot 4.7 before design.
3520
+
3521
+ func _control_create(params: Dictionary) -> Dictionary:
3522
+ var root := _edited_root()
3523
+ if root == null:
3524
+ return _err("no_scene", "No scene is open")
3525
+ var parent := _resolve(root, String(params.get("parent_path", "")))
3526
+ if parent == null:
3527
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
3528
+ var type := String(params.get("type", "Control"))
3529
+ if not ClassDB.can_instantiate(type):
3530
+ return _err("bad_type", "Cannot instantiate class: %s" % type)
3531
+ if not ClassDB.is_parent_class(type, "Control"):
3532
+ return _err("bad_type", "%s is not a Control subclass" % type)
3533
+ var node: Node = ClassDB.instantiate(type)
3534
+ node.name = String(params.get("name", type))
3535
+ if params.has("text") and _has_property(node, "text"):
3536
+ node.set("text", String(params.get("text")))
3537
+ var ur := _plugin.get_undo_redo()
3538
+ ur.create_action("Breakpoint: add %s" % node.name)
3539
+ ur.add_do_method(parent, "add_child", node)
3540
+ ur.add_do_method(node, "set_owner", root)
3541
+ ur.add_do_reference(node)
3542
+ ur.add_undo_method(parent, "remove_child", node)
3543
+ ur.commit_action()
3544
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": type})
3545
+
3546
+
3547
+ func _container_add_child(params: Dictionary) -> Dictionary:
3548
+ var root := _edited_root()
3549
+ if root == null:
3550
+ return _err("no_scene", "No scene is open")
3551
+ var parent := _resolve(root, String(params.get("container_path", "")))
3552
+ if parent == null:
3553
+ return _err("bad_path", "Container not found: %s" % params.get("container_path", ""))
3554
+ if not (parent is Container):
3555
+ return _err("bad_type", "%s is not a Container" % parent.name)
3556
+ var type := String(params.get("type", "Control"))
3557
+ if not ClassDB.can_instantiate(type):
3558
+ return _err("bad_type", "Cannot instantiate class: %s" % type)
3559
+ if not ClassDB.is_parent_class(type, "Control"):
3560
+ return _err("bad_type", "%s is not a Control subclass" % type)
3561
+ var node: Node = ClassDB.instantiate(type)
3562
+ node.name = String(params.get("name", type))
3563
+ var ur := _plugin.get_undo_redo()
3564
+ ur.create_action("Breakpoint: add %s to container" % node.name)
3565
+ ur.add_do_method(parent, "add_child", node)
3566
+ ur.add_do_method(node, "set_owner", root)
3567
+ ur.add_do_reference(node)
3568
+ ur.add_undo_method(parent, "remove_child", node)
3569
+ ur.commit_action()
3570
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": type, "container": _path_of(root, parent)})
3571
+
3572
+
3573
+ func _control_set_anchors(params: Dictionary) -> Dictionary:
3574
+ var root := _edited_root()
3575
+ if root == null:
3576
+ return _err("no_scene", "No scene is open")
3577
+ var node := _resolve(root, String(params.get("path", "")))
3578
+ if node == null:
3579
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3580
+ if not (node is Control):
3581
+ return _err("bad_type", "%s is not a Control" % node.name)
3582
+ var ctrl := node as Control
3583
+ var sides := {"left": "anchor_left", "top": "anchor_top", "right": "anchor_right", "bottom": "anchor_bottom"}
3584
+ var changed := []
3585
+ for k in sides:
3586
+ if params.has(k):
3587
+ changed.append(k)
3588
+ if changed.is_empty():
3589
+ return _err("bad_params", "Provide at least one of left/top/right/bottom")
3590
+ var olds := {}
3591
+ for k in changed:
3592
+ olds[k] = ctrl.get(sides[k])
3593
+ var ur := _plugin.get_undo_redo()
3594
+ ur.create_action("Breakpoint: set %s anchors" % ctrl.name)
3595
+ for k in changed:
3596
+ var prop: String = sides[k]
3597
+ ur.add_do_property(ctrl, prop, float(params.get(k)))
3598
+ ur.add_undo_property(ctrl, prop, olds[k])
3599
+ ur.commit_action()
3600
+ return _ok({
3601
+ "path": _path_of(root, ctrl),
3602
+ "anchors": {
3603
+ "left": ctrl.anchor_left,
3604
+ "top": ctrl.anchor_top,
3605
+ "right": ctrl.anchor_right,
3606
+ "bottom": ctrl.anchor_bottom,
3607
+ },
3608
+ })
3609
+
3610
+
3611
+ func _control_set_layout_preset(params: Dictionary) -> Dictionary:
3612
+ var root := _edited_root()
3613
+ if root == null:
3614
+ return _err("no_scene", "No scene is open")
3615
+ var node := _resolve(root, String(params.get("path", "")))
3616
+ if node == null:
3617
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3618
+ if not (node is Control):
3619
+ return _err("bad_type", "%s is not a Control" % node.name)
3620
+ var ctrl := node as Control
3621
+ var preset := _layout_preset(params.get("preset"))
3622
+ if preset < 0:
3623
+ return _err("bad_params", "Unknown layout preset: %s" % params.get("preset"))
3624
+ var resize_mode := int(params.get("resize_mode", 0))
3625
+ var margin := int(params.get("margin", 0))
3626
+ var props := ["anchor_left", "anchor_top", "anchor_right", "anchor_bottom", "offset_left", "offset_top", "offset_right", "offset_bottom"]
3627
+ var olds := {}
3628
+ for p in props:
3629
+ olds[p] = ctrl.get(p)
3630
+ var ur := _plugin.get_undo_redo()
3631
+ ur.create_action("Breakpoint: layout preset on %s" % ctrl.name)
3632
+ ur.add_do_method(ctrl, "set_anchors_and_offsets_preset", preset, resize_mode, margin)
3633
+ for p in props:
3634
+ ur.add_undo_property(ctrl, p, olds[p])
3635
+ ur.commit_action()
3636
+ return _ok({"path": _path_of(root, ctrl), "preset": preset, "preset_name": _layout_preset_name(preset)})
3637
+
3638
+
3639
+ func _control_set_size_flags(params: Dictionary) -> Dictionary:
3640
+ var root := _edited_root()
3641
+ if root == null:
3642
+ return _err("no_scene", "No scene is open")
3643
+ var node := _resolve(root, String(params.get("path", "")))
3644
+ if node == null:
3645
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3646
+ if not (node is Control):
3647
+ return _err("bad_type", "%s is not a Control" % node.name)
3648
+ var ctrl := node as Control
3649
+ if not (params.has("horizontal") or params.has("vertical") or params.has("stretch_ratio")):
3650
+ return _err("bad_params", "Provide at least one of horizontal/vertical/stretch_ratio")
3651
+ var ur := _plugin.get_undo_redo()
3652
+ ur.create_action("Breakpoint: set %s size flags" % ctrl.name)
3653
+ if params.has("horizontal"):
3654
+ var h_old := ctrl.size_flags_horizontal
3655
+ ur.add_do_property(ctrl, "size_flags_horizontal", int(params.get("horizontal")))
3656
+ ur.add_undo_property(ctrl, "size_flags_horizontal", h_old)
3657
+ if params.has("vertical"):
3658
+ var v_old := ctrl.size_flags_vertical
3659
+ ur.add_do_property(ctrl, "size_flags_vertical", int(params.get("vertical")))
3660
+ ur.add_undo_property(ctrl, "size_flags_vertical", v_old)
3661
+ if params.has("stretch_ratio"):
3662
+ var r_old := ctrl.size_flags_stretch_ratio
3663
+ ur.add_do_property(ctrl, "size_flags_stretch_ratio", float(params.get("stretch_ratio")))
3664
+ ur.add_undo_property(ctrl, "size_flags_stretch_ratio", r_old)
3665
+ ur.commit_action()
3666
+ return _ok({"path": _path_of(root, ctrl), "horizontal": ctrl.size_flags_horizontal, "vertical": ctrl.size_flags_vertical, "stretch_ratio": ctrl.size_flags_stretch_ratio})
3667
+
3668
+
3669
+ func _control_set_theme(params: Dictionary) -> Dictionary:
3670
+ var root := _edited_root()
3671
+ if root == null:
3672
+ return _err("no_scene", "No scene is open")
3673
+ var node := _resolve(root, String(params.get("path", "")))
3674
+ if node == null:
3675
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3676
+ if not (node is Control):
3677
+ return _err("bad_type", "%s is not a Control" % node.name)
3678
+ var ctrl := node as Control
3679
+ var theme_path := String(params.get("theme_path", ""))
3680
+ var theme_res: Theme = null
3681
+ if theme_path != "":
3682
+ if not ResourceLoader.exists(theme_path):
3683
+ return _err("not_found", "Theme not found: %s" % theme_path)
3684
+ var res = ResourceLoader.load(theme_path)
3685
+ if not (res is Theme):
3686
+ return _err("bad_type", "%s is not a Theme" % theme_path)
3687
+ theme_res = res as Theme
3688
+ var old_theme = ctrl.get("theme")
3689
+ var ur := _plugin.get_undo_redo()
3690
+ ur.create_action("Breakpoint: set %s theme" % ctrl.name)
3691
+ ur.add_do_property(ctrl, "theme", theme_res)
3692
+ ur.add_undo_property(ctrl, "theme", old_theme)
3693
+ if theme_res != null:
3694
+ ur.add_do_reference(theme_res)
3695
+ ur.commit_action()
3696
+ return _ok({"path": _path_of(root, ctrl), "theme_path": theme_path})
3697
+
3698
+
3699
+ func _theme_create(params: Dictionary) -> Dictionary:
3700
+ var to_path := String(params.get("to_path", ""))
3701
+ if not to_path.begins_with("res://"):
3702
+ return _err("bad_params", "'to_path' must be a res:// path")
3703
+ var theme := Theme.new()
3704
+ var e := ResourceSaver.save(theme, to_path)
3705
+ if e != OK:
3706
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
3707
+ return _ok({"created": to_path, "type": "Theme"})
3708
+
3709
+
3710
+ func _theme_set_color(params: Dictionary) -> Dictionary:
3711
+ var path := String(params.get("path", ""))
3712
+ var theme = _theme_load(path)
3713
+ if theme == null:
3714
+ return _err("not_found", "Theme not found or not a Theme: %s" % path)
3715
+ var iname := String(params.get("name", ""))
3716
+ var ttype := String(params.get("theme_type", ""))
3717
+ if iname == "" or ttype == "":
3718
+ return _err("bad_params", "Missing 'name' or 'theme_type'")
3719
+ var col := _to_color(params.get("color"))
3720
+ theme.set_color(iname, ttype, col)
3721
+ var e := ResourceSaver.save(theme, path)
3722
+ if e != OK:
3723
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
3724
+ return _ok({"path": path, "name": iname, "theme_type": ttype, "color": [col.r, col.g, col.b, col.a]})
3725
+
3726
+
3727
+ func _theme_set_font(params: Dictionary) -> Dictionary:
3728
+ var path := String(params.get("path", ""))
3729
+ var theme = _theme_load(path)
3730
+ if theme == null:
3731
+ return _err("not_found", "Theme not found or not a Theme: %s" % path)
3732
+ var iname := String(params.get("name", ""))
3733
+ var ttype := String(params.get("theme_type", ""))
3734
+ if iname == "" or ttype == "":
3735
+ return _err("bad_params", "Missing 'name' or 'theme_type'")
3736
+ var font_path := String(params.get("font_path", ""))
3737
+ if not ResourceLoader.exists(font_path):
3738
+ return _err("not_found", "Font not found: %s" % font_path)
3739
+ var fres = ResourceLoader.load(font_path)
3740
+ if not (fres is Font):
3741
+ return _err("bad_type", "%s is not a Font" % font_path)
3742
+ theme.set_font(iname, ttype, fres as Font)
3743
+ var e := ResourceSaver.save(theme, path)
3744
+ if e != OK:
3745
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
3746
+ return _ok({"path": path, "name": iname, "theme_type": ttype, "font_path": font_path})
3747
+
3748
+
3749
+ func _theme_set_stylebox(params: Dictionary) -> Dictionary:
3750
+ var path := String(params.get("path", ""))
3751
+ var theme = _theme_load(path)
3752
+ if theme == null:
3753
+ return _err("not_found", "Theme not found or not a Theme: %s" % path)
3754
+ var iname := String(params.get("name", ""))
3755
+ var ttype := String(params.get("theme_type", ""))
3756
+ if iname == "" or ttype == "":
3757
+ return _err("bad_params", "Missing 'name' or 'theme_type'")
3758
+ var sb_path := String(params.get("stylebox_path", ""))
3759
+ if not ResourceLoader.exists(sb_path):
3760
+ return _err("not_found", "StyleBox not found: %s" % sb_path)
3761
+ var sres = ResourceLoader.load(sb_path)
3762
+ if not (sres is StyleBox):
3763
+ return _err("bad_type", "%s is not a StyleBox" % sb_path)
3764
+ theme.set_stylebox(iname, ttype, sres as StyleBox)
3765
+ var e := ResourceSaver.save(theme, path)
3766
+ if e != OK:
3767
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
3768
+ return _ok({"path": path, "name": iname, "theme_type": ttype, "stylebox_path": sb_path})
3769
+
3770
+
3771
+ func _theme_set_constant(params: Dictionary) -> Dictionary:
3772
+ var path := String(params.get("path", ""))
3773
+ var theme = _theme_load(path)
3774
+ if theme == null:
3775
+ return _err("not_found", "Theme not found or not a Theme: %s" % path)
3776
+ var iname := String(params.get("name", ""))
3777
+ var ttype := String(params.get("theme_type", ""))
3778
+ if iname == "" or ttype == "":
3779
+ return _err("bad_params", "Missing 'name' or 'theme_type'")
3780
+ if not params.has("value"):
3781
+ return _err("bad_params", "Missing 'value'")
3782
+ var val := int(params.get("value"))
3783
+ theme.set_constant(iname, ttype, val)
3784
+ var e := ResourceSaver.save(theme, path)
3785
+ if e != OK:
3786
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
3787
+ return _ok({"path": path, "name": iname, "theme_type": ttype, "value": val})
3788
+
3789
+
3790
+ func _theme_load(path: String):
3791
+ if not ResourceLoader.exists(path):
3792
+ return null
3793
+ var res = ResourceLoader.load(path)
3794
+ if res is Theme:
3795
+ return res
3796
+ return null
3797
+
3798
+
3799
+ func _has_property(obj: Object, prop: String) -> bool:
3800
+ for p in obj.get_property_list():
3801
+ if String(p.get("name", "")) == prop:
3802
+ return true
3803
+ return false
3804
+
3805
+
3806
+ func _layout_preset_names() -> Dictionary:
3807
+ return {
3808
+ "top_left": Control.PRESET_TOP_LEFT,
3809
+ "top_right": Control.PRESET_TOP_RIGHT,
3810
+ "bottom_left": Control.PRESET_BOTTOM_LEFT,
3811
+ "bottom_right": Control.PRESET_BOTTOM_RIGHT,
3812
+ "center_left": Control.PRESET_CENTER_LEFT,
3813
+ "center_top": Control.PRESET_CENTER_TOP,
3814
+ "center_right": Control.PRESET_CENTER_RIGHT,
3815
+ "center_bottom": Control.PRESET_CENTER_BOTTOM,
3816
+ "center": Control.PRESET_CENTER,
3817
+ "left_wide": Control.PRESET_LEFT_WIDE,
3818
+ "top_wide": Control.PRESET_TOP_WIDE,
3819
+ "right_wide": Control.PRESET_RIGHT_WIDE,
3820
+ "bottom_wide": Control.PRESET_BOTTOM_WIDE,
3821
+ "vcenter_wide": Control.PRESET_VCENTER_WIDE,
3822
+ "hcenter_wide": Control.PRESET_HCENTER_WIDE,
3823
+ "full_rect": Control.PRESET_FULL_RECT,
3824
+ }
3825
+
3826
+
3827
+ func _layout_preset(v) -> int:
3828
+ if v is String:
3829
+ var names := _layout_preset_names()
3830
+ var key := String(v).to_lower()
3831
+ if names.has(key):
3832
+ return int(names[key])
3833
+ return -1
3834
+ if v is float or v is int:
3835
+ var iv := int(v)
3836
+ if iv >= 0 and iv <= 15:
3837
+ return iv
3838
+ return -1
3839
+ return -1
3840
+
3841
+
3842
+ func _layout_preset_name(preset: int) -> String:
3843
+ var names := _layout_preset_names()
3844
+ for k in names:
3845
+ if int(names[k]) == preset:
3846
+ return k
3847
+ return str(preset)
3848
+
3849
+
3850
+ # ---------------------------------------------------------------- Group H: 3D & navigation ----
3851
+ # meshinstance / mesh / light / camera / csg / navregion / navagent tools mutate the edited scene
3852
+ # (3D nodes), undoable via EditorUndoRedoManager and ungated (the node_* model). primitive_mesh /
3853
+ # environment author a resource on disk via ResourceSaver and are host-gated file-writers like
3854
+ # resource_* / theme_create. Mesh / Light3D / Camera3D / CSG / NavigationRegion3D /
3855
+ # NavigationAgent3D and the PrimitiveMesh / Environment / Sky APIs were probed live on Godot 4.7.
3856
+
3857
+ func _meshinstance_create(params: Dictionary) -> Dictionary:
3858
+ var root := _edited_root()
3859
+ if root == null:
3860
+ return _err("no_scene", "No scene is open")
3861
+ var parent := _resolve(root, String(params.get("parent_path", "")))
3862
+ if parent == null:
3863
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
3864
+ var mesh_path := String(params.get("mesh_path", ""))
3865
+ var mesh_res: Mesh = null
3866
+ if mesh_path != "":
3867
+ if not ResourceLoader.exists(mesh_path):
3868
+ return _err("not_found", "Mesh not found: %s" % mesh_path)
3869
+ var res = ResourceLoader.load(mesh_path)
3870
+ if not (res is Mesh):
3871
+ return _err("bad_type", "%s is not a Mesh" % mesh_path)
3872
+ mesh_res = res as Mesh
3873
+ var node := MeshInstance3D.new()
3874
+ node.name = String(params.get("name", "MeshInstance3D"))
3875
+ if mesh_res != null:
3876
+ node.mesh = mesh_res
3877
+ var ur := _plugin.get_undo_redo()
3878
+ ur.create_action("Breakpoint: add %s" % node.name)
3879
+ ur.add_do_method(parent, "add_child", node)
3880
+ ur.add_do_method(node, "set_owner", root)
3881
+ ur.add_do_reference(node)
3882
+ ur.add_undo_method(parent, "remove_child", node)
3883
+ ur.commit_action()
3884
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": "MeshInstance3D", "mesh_path": mesh_path})
3885
+
3886
+
3887
+ func _mesh_set_surface_material(params: Dictionary) -> Dictionary:
3888
+ var root := _edited_root()
3889
+ if root == null:
3890
+ return _err("no_scene", "No scene is open")
3891
+ var node := _resolve(root, String(params.get("path", "")))
3892
+ if node == null:
3893
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
3894
+ if not (node is MeshInstance3D):
3895
+ return _err("bad_type", "%s is not a MeshInstance3D" % node.name)
3896
+ var mi := node as MeshInstance3D
3897
+ var material_path := String(params.get("material_path", ""))
3898
+ if material_path == "":
3899
+ return _err("bad_params", "Missing 'material_path'")
3900
+ if not ResourceLoader.exists(material_path):
3901
+ return _err("not_found", "Material not found: %s" % material_path)
3902
+ var mres = ResourceLoader.load(material_path)
3903
+ if not (mres is Material):
3904
+ return _err("bad_type", "%s is not a Material" % material_path)
3905
+ var mat := mres as Material
3906
+ var surface := int(params.get("surface", -1))
3907
+ if surface >= 0:
3908
+ var count := mi.get_surface_override_material_count()
3909
+ if surface >= count:
3910
+ return _err("bad_params", "surface %d out of range (0..%d)" % [surface, count - 1])
3911
+ var ur := _plugin.get_undo_redo()
3912
+ ur.create_action("Breakpoint: set %s material" % mi.name)
3913
+ if surface < 0:
3914
+ var old_mat = mi.material_override
3915
+ ur.add_do_property(mi, "material_override", mat)
3916
+ ur.add_undo_property(mi, "material_override", old_mat)
3917
+ else:
3918
+ var old_sm = mi.get_surface_override_material(surface)
3919
+ ur.add_do_method(mi, "set_surface_override_material", surface, mat)
3920
+ ur.add_undo_method(mi, "set_surface_override_material", surface, old_sm)
3921
+ ur.add_do_reference(mat)
3922
+ ur.commit_action()
3923
+ return _ok({"path": _path_of(root, mi), "material_path": material_path, "surface": surface})
3924
+
3925
+
3926
+ func _primitive_mesh_create(params: Dictionary) -> Dictionary:
3927
+ var to_path := String(params.get("to_path", ""))
3928
+ if not to_path.begins_with("res://"):
3929
+ return _err("bad_params", "'to_path' must be a res:// path")
3930
+ var shape := String(params.get("shape", "box")).to_lower()
3931
+ var classes := {"box": "BoxMesh", "sphere": "SphereMesh", "cylinder": "CylinderMesh", "plane": "PlaneMesh", "capsule": "CapsuleMesh", "prism": "PrismMesh", "torus": "TorusMesh", "quad": "QuadMesh"}
3932
+ if not classes.has(shape):
3933
+ return _err("bad_params", "Unknown primitive shape: %s (box/sphere/cylinder/plane/capsule/prism/torus/quad)" % shape)
3934
+ var cls := String(classes[shape])
3935
+ var mesh: Mesh = ClassDB.instantiate(cls)
3936
+ if mesh == null:
3937
+ return _err("create_failed", "Could not instantiate %s" % cls)
3938
+ var e := ResourceSaver.save(mesh, to_path)
3939
+ if e != OK:
3940
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
3941
+ return _ok({"created": to_path, "type": cls, "shape": shape})
3942
+
3943
+
3944
+ func _light_create(params: Dictionary) -> Dictionary:
3945
+ var root := _edited_root()
3946
+ if root == null:
3947
+ return _err("no_scene", "No scene is open")
3948
+ var parent := _resolve(root, String(params.get("parent_path", "")))
3949
+ if parent == null:
3950
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
3951
+ var kind := String(params.get("kind", "omni")).to_lower()
3952
+ var classes := {"dir": "DirectionalLight3D", "directional": "DirectionalLight3D", "omni": "OmniLight3D", "spot": "SpotLight3D"}
3953
+ if not classes.has(kind):
3954
+ return _err("bad_params", "Unknown light kind: %s (use dir/omni/spot)" % kind)
3955
+ var cls := String(classes[kind])
3956
+ var node: Node = ClassDB.instantiate(cls)
3957
+ node.name = String(params.get("name", cls))
3958
+ var ur := _plugin.get_undo_redo()
3959
+ ur.create_action("Breakpoint: add %s" % node.name)
3960
+ ur.add_do_method(parent, "add_child", node)
3961
+ ur.add_do_method(node, "set_owner", root)
3962
+ ur.add_do_reference(node)
3963
+ ur.add_undo_method(parent, "remove_child", node)
3964
+ ur.commit_action()
3965
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": cls, "kind": kind})
3966
+
3967
+
3968
+ func _camera_create(params: Dictionary) -> Dictionary:
3969
+ var root := _edited_root()
3970
+ if root == null:
3971
+ return _err("no_scene", "No scene is open")
3972
+ var parent := _resolve(root, String(params.get("parent_path", "")))
3973
+ if parent == null:
3974
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
3975
+ var node := Camera3D.new()
3976
+ node.name = String(params.get("name", "Camera3D"))
3977
+ if bool(params.get("current", false)):
3978
+ node.current = true
3979
+ var ur := _plugin.get_undo_redo()
3980
+ ur.create_action("Breakpoint: add %s" % node.name)
3981
+ ur.add_do_method(parent, "add_child", node)
3982
+ ur.add_do_method(node, "set_owner", root)
3983
+ ur.add_do_reference(node)
3984
+ ur.add_undo_method(parent, "remove_child", node)
3985
+ ur.commit_action()
3986
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": "Camera3D", "current": node.current})
3987
+
3988
+
3989
+ func _environment_create(params: Dictionary) -> Dictionary:
3990
+ var to_path := String(params.get("to_path", ""))
3991
+ if not to_path.begins_with("res://"):
3992
+ return _err("bad_params", "'to_path' must be a res:// path")
3993
+ var bg := String(params.get("background", "clear_color")).to_lower()
3994
+ var modes := {"clear_color": Environment.BG_CLEAR_COLOR, "color": Environment.BG_COLOR, "sky": Environment.BG_SKY, "canvas": Environment.BG_CANVAS}
3995
+ if not modes.has(bg):
3996
+ return _err("bad_params", "Unknown background: %s (clear_color/color/sky/canvas)" % bg)
3997
+ var env := Environment.new()
3998
+ env.background_mode = int(modes[bg])
3999
+ if params.has("ambient_color"):
4000
+ env.ambient_light_color = _to_color(params.get("ambient_color"))
4001
+ var e := ResourceSaver.save(env, to_path)
4002
+ if e != OK:
4003
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
4004
+ return _ok({"created": to_path, "type": "Environment", "background_mode": bg})
4005
+
4006
+
4007
+ func _environment_set_sky(params: Dictionary) -> Dictionary:
4008
+ var path := String(params.get("path", ""))
4009
+ if not ResourceLoader.exists(path):
4010
+ return _err("not_found", "Environment not found: %s" % path)
4011
+ var res = ResourceLoader.load(path)
4012
+ if not (res is Environment):
4013
+ return _err("bad_type", "%s is not an Environment" % path)
4014
+ var env := res as Environment
4015
+ var mat_kind := String(params.get("sky_material", "procedural")).to_lower()
4016
+ var sky := Sky.new()
4017
+ match mat_kind:
4018
+ "procedural":
4019
+ sky.sky_material = ProceduralSkyMaterial.new()
4020
+ "physical":
4021
+ sky.sky_material = PhysicalSkyMaterial.new()
4022
+ "panorama":
4023
+ sky.sky_material = PanoramaSkyMaterial.new()
4024
+ _:
4025
+ return _err("bad_params", "Unknown sky_material: %s (procedural/physical/panorama)" % mat_kind)
4026
+ env.sky = sky
4027
+ env.background_mode = Environment.BG_SKY
4028
+ var e := ResourceSaver.save(env, path)
4029
+ if e != OK:
4030
+ return _err("save_failed", "ResourceSaver.save() returned %d" % e)
4031
+ return _ok({"path": path, "background_mode": "sky", "sky_material": mat_kind})
4032
+
4033
+
4034
+ func _csg_create(params: Dictionary) -> Dictionary:
4035
+ var root := _edited_root()
4036
+ if root == null:
4037
+ return _err("no_scene", "No scene is open")
4038
+ var parent := _resolve(root, String(params.get("parent_path", "")))
4039
+ if parent == null:
4040
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
4041
+ var shape := String(params.get("shape", "box")).to_lower()
4042
+ var classes := {"box": "CSGBox3D", "sphere": "CSGSphere3D", "cylinder": "CSGCylinder3D", "torus": "CSGTorus3D", "polygon": "CSGPolygon3D", "mesh": "CSGMesh3D", "combiner": "CSGCombiner3D"}
4043
+ if not classes.has(shape):
4044
+ return _err("bad_params", "Unknown CSG shape: %s (box/sphere/cylinder/torus/polygon/mesh/combiner)" % shape)
4045
+ var cls := String(classes[shape])
4046
+ if not ClassDB.can_instantiate(cls):
4047
+ return _err("bad_type", "Cannot instantiate class: %s" % cls)
4048
+ var node: Node = ClassDB.instantiate(cls)
4049
+ node.name = String(params.get("name", cls))
4050
+ var ur := _plugin.get_undo_redo()
4051
+ ur.create_action("Breakpoint: add %s" % node.name)
4052
+ ur.add_do_method(parent, "add_child", node)
4053
+ ur.add_do_method(node, "set_owner", root)
4054
+ ur.add_do_reference(node)
4055
+ ur.add_undo_method(parent, "remove_child", node)
4056
+ ur.commit_action()
4057
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": cls, "shape": shape})
4058
+
4059
+
4060
+ func _navregion_create(params: Dictionary) -> Dictionary:
4061
+ var root := _edited_root()
4062
+ if root == null:
4063
+ return _err("no_scene", "No scene is open")
4064
+ var parent := _resolve(root, String(params.get("parent_path", "")))
4065
+ if parent == null:
4066
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
4067
+ var node := NavigationRegion3D.new()
4068
+ node.name = String(params.get("name", "NavigationRegion3D"))
4069
+ if bool(params.get("with_navmesh", true)):
4070
+ node.navigation_mesh = NavigationMesh.new()
4071
+ var ur := _plugin.get_undo_redo()
4072
+ ur.create_action("Breakpoint: add %s" % node.name)
4073
+ ur.add_do_method(parent, "add_child", node)
4074
+ ur.add_do_method(node, "set_owner", root)
4075
+ ur.add_do_reference(node)
4076
+ ur.add_undo_method(parent, "remove_child", node)
4077
+ ur.commit_action()
4078
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": "NavigationRegion3D", "has_navmesh": node.navigation_mesh != null})
4079
+
4080
+
4081
+ func _navagent_configure(params: Dictionary) -> Dictionary:
4082
+ var root := _edited_root()
4083
+ if root == null:
4084
+ return _err("no_scene", "No scene is open")
4085
+ var parent := _resolve(root, String(params.get("parent_path", "")))
4086
+ if parent == null:
4087
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
4088
+ var node := NavigationAgent3D.new()
4089
+ node.name = String(params.get("name", "NavigationAgent3D"))
4090
+ if params.has("radius"):
4091
+ node.radius = float(params.get("radius"))
4092
+ if params.has("height"):
4093
+ node.height = float(params.get("height"))
4094
+ if params.has("max_speed"):
4095
+ node.max_speed = float(params.get("max_speed"))
4096
+ if params.has("path_desired_distance"):
4097
+ node.path_desired_distance = float(params.get("path_desired_distance"))
4098
+ if params.has("target_desired_distance"):
4099
+ node.target_desired_distance = float(params.get("target_desired_distance"))
4100
+ if params.has("avoidance_enabled"):
4101
+ node.avoidance_enabled = bool(params.get("avoidance_enabled"))
4102
+ var ur := _plugin.get_undo_redo()
4103
+ ur.create_action("Breakpoint: add %s" % node.name)
4104
+ ur.add_do_method(parent, "add_child", node)
4105
+ ur.add_do_method(node, "set_owner", root)
4106
+ ur.add_do_reference(node)
4107
+ ur.add_undo_method(parent, "remove_child", node)
4108
+ ur.commit_action()
4109
+ return _ok({"path": _path_of(root, node), "name": String(node.name), "type": "NavigationAgent3D", "config": {"radius": node.radius, "height": node.height, "max_speed": node.max_speed, "path_desired_distance": node.path_desired_distance, "target_desired_distance": node.target_desired_distance, "avoidance_enabled": node.avoidance_enabled}})
4110
+
4111
+
4112
+ # ------------------------------------------------ Group I: input/config/testing ----
4113
+
4114
+ func _keycode_of(v) -> int:
4115
+ if v is String:
4116
+ return OS.find_keycode_from_string(String(v))
4117
+ return int(v)
4118
+
4119
+
4120
+ func _make_input_event(spec: Dictionary) -> InputEvent:
4121
+ var t := String(spec.get("type", "")).to_lower()
4122
+ match t:
4123
+ "key":
4124
+ var ek := InputEventKey.new()
4125
+ if spec.has("physical_keycode"):
4126
+ ek.physical_keycode = _keycode_of(spec.get("physical_keycode"))
4127
+ else:
4128
+ ek.keycode = _keycode_of(spec.get("keycode"))
4129
+ return ek
4130
+ "mouse_button":
4131
+ var emb := InputEventMouseButton.new()
4132
+ emb.button_index = int(spec.get("button_index", 1))
4133
+ return emb
4134
+ "joy_button":
4135
+ var ejb := InputEventJoypadButton.new()
4136
+ ejb.button_index = int(spec.get("button_index", 0))
4137
+ return ejb
4138
+ "joy_motion":
4139
+ var ejm := InputEventJoypadMotion.new()
4140
+ ejm.axis = int(spec.get("axis", 0))
4141
+ ejm.axis_value = float(spec.get("axis_value", 1.0))
4142
+ return ejm
4143
+ return null
4144
+
4145
+
4146
+ func _inputmap_add_action(params: Dictionary) -> Dictionary:
4147
+ var aname := String(params.get("name", ""))
4148
+ if aname == "":
4149
+ return _err("bad_params", "Missing 'name'")
4150
+ var key := "input/" + aname
4151
+ var deadzone := float(params.get("deadzone", 0.5))
4152
+ ProjectSettings.set_setting(key, {"deadzone": deadzone, "events": []})
4153
+ var saved := false
4154
+ if bool(params.get("save", false)):
4155
+ var e := ProjectSettings.save()
4156
+ if e != OK:
4157
+ return _err("save_failed", "ProjectSettings.save() returned %d" % e)
4158
+ saved = true
4159
+ return _ok({"action": aname, "deadzone": deadzone, "saved": saved})
4160
+
4161
+
4162
+ func _inputmap_add_event(params: Dictionary) -> Dictionary:
4163
+ var aname := String(params.get("name", ""))
4164
+ if aname == "":
4165
+ return _err("bad_params", "Missing 'name'")
4166
+ var key := "input/" + aname
4167
+ if not ProjectSettings.has_setting(key):
4168
+ return _err("not_found", "Input action not found: %s" % aname)
4169
+ var spec = params.get("event")
4170
+ if not (spec is Dictionary):
4171
+ return _err("bad_params", "Missing or invalid 'event' object")
4172
+ var ev := _make_input_event(spec)
4173
+ if ev == null:
4174
+ return _err("bad_params", "Unsupported event (use type key/mouse_button/joy_button/joy_motion)")
4175
+ var action = ProjectSettings.get_setting(key)
4176
+ if not (action is Dictionary):
4177
+ return _err("bad_type", "Input action %s is malformed" % aname)
4178
+ var events: Array = action.get("events", [])
4179
+ events.append(ev)
4180
+ action["events"] = events
4181
+ ProjectSettings.set_setting(key, action)
4182
+ var saved := false
4183
+ if bool(params.get("save", false)):
4184
+ var e := ProjectSettings.save()
4185
+ if e != OK:
4186
+ return _err("save_failed", "ProjectSettings.save() returned %d" % e)
4187
+ saved = true
4188
+ return _ok({"action": aname, "event_count": events.size(), "event_class": ev.get_class(), "saved": saved})
4189
+
4190
+
4191
+ func _inputmap_list(_params: Dictionary) -> Dictionary:
4192
+ var actions: Array = []
4193
+ for prop in ProjectSettings.get_property_list():
4194
+ var pn := String(prop.get("name", ""))
4195
+ if not pn.begins_with("input/"):
4196
+ continue
4197
+ var an := pn.substr(6)
4198
+ if an == "":
4199
+ continue
4200
+ var action = ProjectSettings.get_setting(pn)
4201
+ var deadzone := 0.5
4202
+ var evlist: Array = []
4203
+ if action is Dictionary:
4204
+ deadzone = float(action.get("deadzone", 0.5))
4205
+ for ev in action.get("events", []):
4206
+ if ev is InputEvent:
4207
+ evlist.append({"class": ev.get_class(), "text": ev.as_text()})
4208
+ else:
4209
+ evlist.append({"class": "unknown", "text": str(ev)})
4210
+ actions.append({"name": an, "deadzone": deadzone, "events": evlist})
4211
+ return _ok({"count": actions.size(), "actions": actions})
4212
+
4213
+
4214
+ func _inputmap_erase_action(params: Dictionary) -> Dictionary:
4215
+ var aname := String(params.get("name", ""))
4216
+ if aname == "":
4217
+ return _err("bad_params", "Missing 'name'")
4218
+ var key := "input/" + aname
4219
+ var existed := ProjectSettings.has_setting(key)
4220
+ if existed:
4221
+ ProjectSettings.set_setting(key, null)
4222
+ var saved := false
4223
+ if bool(params.get("save", false)):
4224
+ var e := ProjectSettings.save()
4225
+ if e != OK:
4226
+ return _err("save_failed", "ProjectSettings.save() returned %d" % e)
4227
+ saved = true
4228
+ return _ok({"erased": existed, "action": aname, "saved": saved})
4229
+
4230
+
4231
+ func _project_add_autoload(params: Dictionary) -> Dictionary:
4232
+ var aname := String(params.get("name", ""))
4233
+ var apath := String(params.get("path", ""))
4234
+ if aname == "" or apath == "":
4235
+ return _err("bad_params", "Missing 'name' or 'path'")
4236
+ if not (ResourceLoader.exists(apath) or FileAccess.file_exists(apath)):
4237
+ return _err("not_found", "Autoload path not found: %s" % apath)
4238
+ var enabled := bool(params.get("enabled", true))
4239
+ var value := ("*" if enabled else "") + apath
4240
+ ProjectSettings.set_setting("autoload/" + aname, value)
4241
+ var saved := false
4242
+ if bool(params.get("save", false)):
4243
+ var e := ProjectSettings.save()
4244
+ if e != OK:
4245
+ return _err("save_failed", "ProjectSettings.save() returned %d" % e)
4246
+ saved = true
4247
+ return _ok({"autoload": aname, "path": apath, "enabled": enabled, "saved": saved})
4248
+
4249
+
4250
+ func _project_remove_autoload(params: Dictionary) -> Dictionary:
4251
+ var aname := String(params.get("name", ""))
4252
+ if aname == "":
4253
+ return _err("bad_params", "Missing 'name'")
4254
+ var key := "autoload/" + aname
4255
+ var existed := ProjectSettings.has_setting(key)
4256
+ if existed:
4257
+ ProjectSettings.set_setting(key, null)
4258
+ var saved := false
4259
+ if bool(params.get("save", false)):
4260
+ var e := ProjectSettings.save()
4261
+ if e != OK:
4262
+ return _err("save_failed", "ProjectSettings.save() returned %d" % e)
4263
+ saved = true
4264
+ return _ok({"removed": existed, "autoload": aname, "saved": saved})
4265
+
4266
+
4267
+ func _project_add_export_preset(params: Dictionary) -> Dictionary:
4268
+ var pname := String(params.get("name", ""))
4269
+ var platform := String(params.get("platform", ""))
4270
+ if pname == "" or platform == "":
4271
+ return _err("bad_params", "Missing 'name' or 'platform'")
4272
+ var cfg_path := "res://export_presets.cfg"
4273
+ var cfg := ConfigFile.new()
4274
+ cfg.load(cfg_path)
4275
+ var idx := 0
4276
+ while cfg.has_section("preset." + str(idx)):
4277
+ idx += 1
4278
+ var section := "preset." + str(idx)
4279
+ cfg.set_value(section, "name", pname)
4280
+ cfg.set_value(section, "platform", platform)
4281
+ cfg.set_value(section, "runnable", bool(params.get("runnable", true)))
4282
+ cfg.set_value(section, "dedicated_server", false)
4283
+ cfg.set_value(section, "custom_features", "")
4284
+ cfg.set_value(section, "export_filter", "all_resources")
4285
+ cfg.set_value(section, "include_filter", "")
4286
+ cfg.set_value(section, "exclude_filter", "")
4287
+ cfg.set_value(section, "export_path", String(params.get("export_path", "")))
4288
+ cfg.set_value(section, "encryption_include_filters", "")
4289
+ cfg.set_value(section, "encryption_exclude_filters", "")
4290
+ cfg.set_value(section, "encrypt_pck", false)
4291
+ cfg.set_value(section, "encrypt_directory", false)
4292
+ cfg.set_value(section + ".options", "custom_template/debug", "")
4293
+ cfg.set_value(section + ".options", "custom_template/release", "")
4294
+ var e := cfg.save(cfg_path)
4295
+ if e != OK:
4296
+ return _err("save_failed", "ConfigFile.save() returned %d" % e)
4297
+ return _ok({"preset": pname, "platform": platform, "index": idx, "path": cfg_path})
4298
+
4299
+
4300
+ func _project_set_main_scene(params: Dictionary) -> Dictionary:
4301
+ var spath := String(params.get("path", ""))
4302
+ if spath == "":
4303
+ return _err("bad_params", "Missing 'path'")
4304
+ if not ResourceLoader.exists(spath):
4305
+ return _err("not_found", "Scene not found: %s" % spath)
4306
+ if not (spath.ends_with(".tscn") or spath.ends_with(".scn")):
4307
+ return _err("bad_params", "Main scene must be a .tscn/.scn: %s" % spath)
4308
+ ProjectSettings.set_setting("application/run/main_scene", spath)
4309
+ var saved := false
4310
+ if bool(params.get("save", false)):
4311
+ var e := ProjectSettings.save()
4312
+ if e != OK:
4313
+ return _err("save_failed", "ProjectSettings.save() returned %d" % e)
4314
+ saved = true
4315
+ return _ok({"main_scene": spath, "saved": saved})
4316
+
4317
+
4318
+ func _project_list_settings(params: Dictionary) -> Dictionary:
4319
+ var prefix := String(params.get("prefix", ""))
4320
+ var out: Array = []
4321
+ for prop in ProjectSettings.get_property_list():
4322
+ var pn := String(prop.get("name", ""))
4323
+ if pn == "":
4324
+ continue
4325
+ if prefix != "" and not pn.begins_with(prefix):
4326
+ continue
4327
+ if not ProjectSettings.has_setting(pn):
4328
+ continue
4329
+ out.append({"name": pn, "value": Codec.encode(ProjectSettings.get_setting(pn))})
4330
+ return _ok({"prefix": prefix, "count": out.size(), "settings": out})
4331
+
4332
+
4333
+ func _editorsettings_get_set(params: Dictionary) -> Dictionary:
4334
+ var sname := String(params.get("name", ""))
4335
+ if sname == "":
4336
+ return _err("bad_params", "Missing 'name'")
4337
+ var es := EditorInterface.get_editor_settings()
4338
+ if es == null:
4339
+ return _err("unsupported", "EditorSettings unavailable")
4340
+ if params.has("value"):
4341
+ es.set_setting(sname, Codec.decode(params.get("value")))
4342
+ return _ok({"name": sname, "value": Codec.encode(es.get_setting(sname)), "mode": "set"})
4343
+ if not es.has_setting(sname):
4344
+ return _err("not_found", "Editor setting not found: %s" % sname)
4345
+ return _ok({"name": sname, "value": Codec.encode(es.get_setting(sname)), "mode": "get"})
4346
+
4347
+
4348
+ func _test_detect(_params: Dictionary) -> Dictionary:
4349
+ var frameworks := [
4350
+ {"name": "gut", "dir": "res://addons/gut", "cfg": "res://addons/gut/plugin.cfg"},
4351
+ {"name": "gdunit4", "dir": "res://addons/gdUnit4", "cfg": "res://addons/gdUnit4/plugin.cfg"},
4352
+ ]
4353
+ for fw in frameworks:
4354
+ if DirAccess.dir_exists_absolute(fw["dir"]):
4355
+ var version := ""
4356
+ var cfg := ConfigFile.new()
4357
+ if cfg.load(fw["cfg"]) == OK:
4358
+ version = String(cfg.get_value("plugin", "version", ""))
4359
+ return _ok({"framework": fw["name"], "path": fw["dir"], "version": version})
4360
+ return _ok({"framework": "none", "path": "", "version": ""})
4361
+
4362
+
4363
+ func _is_test_script(fname: String) -> bool:
4364
+ if not fname.ends_with(".gd"):
4365
+ return false
4366
+ return fname.begins_with("test_") or fname.ends_with("_test.gd")
4367
+
4368
+
4369
+ func _collect_tests(dir_path: String, out: Array) -> void:
4370
+ var d := DirAccess.open(dir_path)
4371
+ if d == null:
4372
+ return
4373
+ d.list_dir_begin()
4374
+ var entry := d.get_next()
4375
+ while entry != "":
4376
+ if entry != "." and entry != "..":
4377
+ var full := dir_path.path_join(entry)
4378
+ if d.current_is_dir():
4379
+ _collect_tests(full, out)
4380
+ elif _is_test_script(entry):
4381
+ out.append(full)
4382
+ entry = d.get_next()
4383
+ d.list_dir_end()
4384
+
4385
+
4386
+ func _test_list(params: Dictionary) -> Dictionary:
4387
+ var dir_path := String(params.get("dir", "res://test"))
4388
+ var tests: Array = []
4389
+ if DirAccess.dir_exists_absolute(dir_path):
4390
+ _collect_tests(dir_path, tests)
4391
+ return _ok({"dir": dir_path, "count": tests.size(), "tests": tests})
4392
+
4393
+
4394
+ # ------------------------------------------------ Group M: netcode scaffolding ----
4395
+ # Node authoring (undoable, EditorUndoRedoManager) for Godot 4's built-in
4396
+ # high-level multiplayer, plus a shared GDScript writer for the host-built codegen
4397
+ # tools. We host nothing — these only add nodes / scripts / config to the project.
4398
+
4399
+
4400
+ func _mp_add_spawner(params: Dictionary) -> Dictionary:
4401
+ var root := _edited_root()
4402
+ if root == null:
4403
+ return _err("no_scene", "No scene is open")
4404
+ var parent := _resolve(root, String(params.get("parent_path", "")))
4405
+ if parent == null:
4406
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
4407
+ var spawner := MultiplayerSpawner.new()
4408
+ spawner.name = String(params.get("name", "MultiplayerSpawner"))
4409
+ var spawn_path_str := String(params.get("spawn_path", ""))
4410
+ if spawn_path_str != "":
4411
+ spawner.spawn_path = NodePath(spawn_path_str)
4412
+ var added_scenes: Array = []
4413
+ for s in params.get("spawnable_scenes", []):
4414
+ var sp := String(s)
4415
+ if sp.begins_with("res://"):
4416
+ spawner.add_spawnable_scene(sp)
4417
+ added_scenes.append(sp)
4418
+ var ur := _plugin.get_undo_redo()
4419
+ ur.create_action("Breakpoint: add %s" % spawner.name)
4420
+ ur.add_do_method(parent, "add_child", spawner)
4421
+ ur.add_do_method(spawner, "set_owner", root)
4422
+ ur.add_do_reference(spawner)
4423
+ ur.add_undo_method(parent, "remove_child", spawner)
4424
+ ur.commit_action()
4425
+ return _ok({
4426
+ "path": _path_of(root, spawner),
4427
+ "name": String(spawner.name),
4428
+ "type": "MultiplayerSpawner",
4429
+ "spawn_path": String(spawner.spawn_path),
4430
+ "spawnable_scenes": added_scenes,
4431
+ })
4432
+
4433
+
4434
+ func _mp_add_synchronizer(params: Dictionary) -> Dictionary:
4435
+ var root := _edited_root()
4436
+ if root == null:
4437
+ return _err("no_scene", "No scene is open")
4438
+ var parent := _resolve(root, String(params.get("parent_path", "")))
4439
+ if parent == null:
4440
+ return _err("bad_path", "Parent not found: %s" % params.get("parent_path", ""))
4441
+ var sync := MultiplayerSynchronizer.new()
4442
+ sync.name = String(params.get("name", "MultiplayerSynchronizer"))
4443
+ var root_path_str := String(params.get("root_path", ""))
4444
+ if root_path_str != "":
4445
+ sync.root_path = NodePath(root_path_str)
4446
+ var mode_str := String(params.get("replication_mode", "always"))
4447
+ var mode := SceneReplicationConfig.REPLICATION_MODE_ALWAYS
4448
+ if mode_str == "on_change":
4449
+ mode = SceneReplicationConfig.REPLICATION_MODE_ON_CHANGE
4450
+ elif mode_str == "never":
4451
+ mode = SceneReplicationConfig.REPLICATION_MODE_NEVER
4452
+ var added_props: Array = []
4453
+ var props: Array = params.get("properties", [])
4454
+ if props.size() > 0:
4455
+ var cfg := SceneReplicationConfig.new()
4456
+ for p in props:
4457
+ var np := NodePath(String(p))
4458
+ cfg.add_property(np)
4459
+ cfg.property_set_replication_mode(np, mode)
4460
+ added_props.append(String(p))
4461
+ sync.replication_config = cfg
4462
+ var ur := _plugin.get_undo_redo()
4463
+ ur.create_action("Breakpoint: add %s" % sync.name)
4464
+ ur.add_do_method(parent, "add_child", sync)
4465
+ ur.add_do_method(sync, "set_owner", root)
4466
+ ur.add_do_reference(sync)
4467
+ ur.add_undo_method(parent, "remove_child", sync)
4468
+ ur.commit_action()
4469
+ return _ok({
4470
+ "path": _path_of(root, sync),
4471
+ "name": String(sync.name),
4472
+ "type": "MultiplayerSynchronizer",
4473
+ "root_path": String(sync.root_path),
4474
+ "properties": added_props,
4475
+ })
4476
+
4477
+
4478
+ func _mp_set_authority(params: Dictionary) -> Dictionary:
4479
+ var root := _edited_root()
4480
+ if root == null:
4481
+ return _err("no_scene", "No scene is open")
4482
+ var node := _resolve(root, String(params.get("path", "")))
4483
+ if node == null:
4484
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
4485
+ var peer_id := int(params.get("peer_id", 1))
4486
+ var recursive := bool(params.get("recursive", true))
4487
+ var previous := node.get_multiplayer_authority()
4488
+ var ur := _plugin.get_undo_redo()
4489
+ ur.create_action("Breakpoint: set authority %s -> %d" % [node.name, peer_id])
4490
+ ur.add_do_method(node, "set_multiplayer_authority", peer_id, recursive)
4491
+ ur.add_undo_method(node, "set_multiplayer_authority", previous, recursive)
4492
+ ur.commit_action()
4493
+ return _ok({"path": _path_of(root, node), "peer_id": peer_id, "previous": previous, "recursive": recursive})
4494
+
4495
+
4496
+ ## Write a host-built GDScript to a res:// .gd path through the editor's FileAccess
4497
+ ## and rescan, so the codegen tools (enet/webrtc peer, @rpc wiring, lobby) land a
4498
+ ## file the editor immediately sees. `require_class` feature-detects (WebRTC): an
4499
+ ## absent class returns status:"unsupported" with nothing written.
4500
+ func _mp_write_script(params: Dictionary) -> Dictionary:
4501
+ var to_path := String(params.get("to_path", ""))
4502
+ if not to_path.begins_with("res://") or not to_path.ends_with(".gd"):
4503
+ return _err("bad_params", "'to_path' must be a res:// .gd path")
4504
+ var require_class := String(params.get("require_class", ""))
4505
+ if require_class != "" and not ClassDB.can_instantiate(require_class):
4506
+ return _ok({"status": "unsupported", "path": null, "required_class": require_class})
4507
+ var overwrite := bool(params.get("overwrite", false))
4508
+ var existed := FileAccess.file_exists(to_path)
4509
+ if existed and not overwrite:
4510
+ return _err("exists", "File already exists (pass overwrite): %s" % to_path)
4511
+ var base_dir := to_path.get_base_dir()
4512
+ if base_dir != "" and base_dir != "res://" and not DirAccess.dir_exists_absolute(base_dir):
4513
+ DirAccess.make_dir_recursive_absolute(base_dir)
4514
+ var content := String(params.get("content", ""))
4515
+ var f := FileAccess.open(to_path, FileAccess.WRITE)
4516
+ if f == null:
4517
+ return _err("write_failed", "Could not open %s for writing (error %d)" % [to_path, FileAccess.get_open_error()])
4518
+ f.store_string(content)
4519
+ f.close()
4520
+ var efs := EditorInterface.get_resource_filesystem()
4521
+ efs.update_file(to_path)
4522
+ efs.scan()
4523
+ return _ok({"status": "written", "path": to_path, "bytes": content.to_utf8_buffer().size(), "created": not existed})
4524
+
4525
+
4526
+ ## Detect which known game-backend SDKs are installed in this project. Each is
4527
+ ## matched by any of: an autoload of a known name, a known addon directory under
4528
+ ## res://addons, or a known global class_name. Read-only — the backend_* / *_scaffold
4529
+ ## codegen tools feature-detect off this so they degrade ("install <SDK> first")
4530
+ ## instead of generating a dead call. We host nothing; we only wire the installed SDK.
4531
+ func _backend_detect(_params: Dictionary) -> Dictionary:
4532
+ # sdk id -> { autoloads: [names], addon: res://path, classes: [class_names] }
4533
+ var known := {
4534
+ "silentwolf": {"autoloads": ["SilentWolf"], "addon": "res://addons/silent_wolf", "classes": []},
4535
+ "nakama": {"autoloads": ["Nakama"], "addon": "res://addons/com.heroiclabs.nakama", "classes": ["Nakama"]},
4536
+ "playfab": {"autoloads": ["PlayFab", "PlayFabManager"], "addon": "res://addons/godot-playfab", "classes": ["PlayFabManager"]},
4537
+ "photon": {"autoloads": ["Photon"], "addon": "res://addons/photon", "classes": []},
4538
+ }
4539
+ # Gather the project's registered global class names (guarded — older builds).
4540
+ var global_classes := {}
4541
+ if ProjectSettings.has_method("get_global_class_list"):
4542
+ for gc in ProjectSettings.get_global_class_list():
4543
+ global_classes[String(gc.get("class", ""))] = true
4544
+ var backends: Array = []
4545
+ var detected: Array = []
4546
+ for sdk in known.keys():
4547
+ var sig: Dictionary = known[sdk]
4548
+ var found_autoload := ""
4549
+ for a in sig["autoloads"]:
4550
+ if ProjectSettings.has_setting("autoload/" + String(a)):
4551
+ found_autoload = String(a)
4552
+ break
4553
+ var addon_dir := String(sig["addon"])
4554
+ var has_addon := DirAccess.dir_exists_absolute(addon_dir)
4555
+ var found_class := ""
4556
+ for c in sig["classes"]:
4557
+ if global_classes.has(String(c)):
4558
+ found_class = String(c)
4559
+ break
4560
+ var method := ""
4561
+ if found_autoload != "":
4562
+ method = "autoload"
4563
+ elif has_addon:
4564
+ method = "addon"
4565
+ elif found_class != "":
4566
+ method = "class"
4567
+ var installed := method != ""
4568
+ if installed:
4569
+ detected.append(String(sdk))
4570
+ backends.append({
4571
+ "sdk": String(sdk),
4572
+ "installed": installed,
4573
+ "method": (method if method != "" else null),
4574
+ "autoload": (found_autoload if found_autoload != "" else null),
4575
+ "addon_dir": (addon_dir if has_addon else null),
4576
+ "class_name": (found_class if found_class != "" else null),
4577
+ })
4578
+ return _ok({"backends": backends, "detected": detected})