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.
- package/addon/breakpoint_mcp/README.md +28 -0
- package/addon/breakpoint_mcp/bridge_server.gd +121 -0
- package/addon/breakpoint_mcp/icon.png +0 -0
- package/addon/breakpoint_mcp/operations.gd +4578 -0
- package/addon/breakpoint_mcp/plugin.cfg +7 -0
- package/addon/breakpoint_mcp/plugin.gd +64 -0
- package/addon/breakpoint_mcp/runtime_bridge.gd +440 -0
- package/addon/breakpoint_mcp/variant_json.gd +108 -0
- package/dist/cli/args.js +40 -0
- package/dist/cli/clients.js +74 -0
- package/dist/cli/doctor.js +269 -0
- package/dist/cli/init.js +199 -0
- package/dist/config.js +6 -30
- package/dist/index.js +49 -4
- package/dist/tools/editor/animation.js +162 -0
- package/dist/tools/editor/audio.js +109 -0
- package/dist/tools/editor/common.js +31 -0
- package/dist/tools/editor/core.js +49 -0
- package/dist/tools/editor/filesystem.js +35 -0
- package/dist/tools/editor/introspection.js +73 -0
- package/dist/tools/editor/node.js +171 -0
- package/dist/tools/editor/particles.js +96 -0
- package/dist/tools/editor/physics.js +236 -0
- package/dist/tools/editor/project_input_test.js +187 -0
- package/dist/tools/editor/resource.js +104 -0
- package/dist/tools/editor/scene.js +95 -0
- package/dist/tools/editor/shader.js +59 -0
- package/dist/tools/editor/signal.js +67 -0
- package/dist/tools/editor/spatial.js +185 -0
- package/dist/tools/editor/tiles.js +160 -0
- package/dist/tools/editor/ui.js +175 -0
- package/dist/tools/editor.js +39 -1914
- package/package.json +4 -2
|
@@ -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})
|