gdharness 1.2.14 → 1.2.16

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.
@@ -17,6 +17,16 @@ extends RefCounted
17
17
  const Read = preload("reading.gd")
18
18
  const Serialisation = preload("serialisation.gd")
19
19
 
20
+ # The data types a Theme names its items by, as in "Button/colors/font_color", and what each holds.
21
+ const THEME_ITEMS: Dictionary[String, Array] = {
22
+ "colors": [TYPE_COLOR, ""],
23
+ "constants": [TYPE_INT, ""],
24
+ "font_sizes": [TYPE_INT, ""],
25
+ "fonts": [TYPE_OBJECT, "Font"],
26
+ "icons": [TYPE_OBJECT, "Texture2D"],
27
+ "styles": [TYPE_OBJECT, "StyleBox"],
28
+ }
29
+
20
30
  # What each packed array holds, for the four whose elements are not types JSON has. A polygon is
21
31
  # written as a list of pairs, and only the property knows that each pair is a Vector2 rather than a
22
32
  # list of two numbers: without being told, every element was read as itself and the engine turned
@@ -394,9 +404,27 @@ func declaration(target: Object, property: String) -> Dictionary:
394
404
  if Read.as_int(declared.get("usage", 0)) & HEADINGS:
395
405
  continue
396
406
  return declared
407
+ if target is Theme:
408
+ return _theme_item(property)
397
409
  return {}
398
410
 
399
411
 
412
+ ## The entry a Theme would declare for [param property] once it held that item, or {} when the name
413
+ ## is not an item's. A Theme lists only the items it holds and takes any other through set, so an
414
+ ## item a read of one theme answered with was refused when written into another that lacked it.
415
+ static func _theme_item(property: String) -> Dictionary:
416
+ var parts: PackedStringArray = property.split("/")
417
+ if parts.size() != 3 or parts[0].is_empty() or parts[2].is_empty() or not THEME_ITEMS.has(parts[1]):
418
+ return {}
419
+ var item: Array = THEME_ITEMS[parts[1]]
420
+ var kind: int = item[0]
421
+ var made: Dictionary = {"name": property, "type": kind, "usage": PROPERTY_USAGE_DEFAULT}
422
+ if kind == TYPE_OBJECT:
423
+ made["hint"] = PROPERTY_HINT_RESOURCE_TYPE
424
+ made["hint_string"] = item[1]
425
+ return made
426
+
427
+
400
428
  ## Whether [param held] is [param given], as a property read back after a set answers it.
401
429
  ##
402
430
  ## Close enough rather than equal for anything made of floats: a Vector2 property stores 32-bit
@@ -124,21 +124,40 @@ func fixed(node: Node) -> String:
124
124
  return ""
125
125
 
126
126
 
127
- ## Why [param child_name] cannot be given to a child of [param parent], or "". Godot renames a node
128
- ## whose name is taken or holds a character a path cannot, and the next call naming it would then
129
- ## reach another node.
130
- func unusable_name(parent: Node, child_name: String, moving: Node = null) -> String:
131
- if child_name.is_empty():
127
+ ## Why the folder a new file at [param file_path] goes in cannot be made, or "" once it is there. The
128
+ ## engine makes no folder when it saves, so a create into one not yet made failed with no more than
129
+ ## the engine's error, while script_edit create made its folder and went ahead.
130
+ static func make_folder_for(file_path: String) -> String:
131
+ var folder: String = file_path.get_base_dir()
132
+ if DirAccess.dir_exists_absolute(folder):
133
+ return ""
134
+ var made: Error = DirAccess.make_dir_recursive_absolute(folder)
135
+ return "" if made == OK else "Could not make the folder %s: %s" % [folder, error_string(made)]
136
+
137
+
138
+ ## Why no node can be called [param node_name], wherever it sits, or "".
139
+ static func unfit_name(node_name: String) -> String:
140
+ if node_name.is_empty():
132
141
  return "A node needs a name."
133
- var valid: String = child_name.validate_node_name()
134
- if valid != child_name:
142
+ var valid: String = node_name.validate_node_name()
143
+ if valid != node_name:
135
144
  return (
136
145
  (
137
146
  "%s is not a name a node can have; Godot would save it as %s. The characters "
138
147
  + '. : @ / " %% cannot be in a node name.'
139
148
  )
140
- % [child_name, valid]
149
+ % [node_name, valid]
141
150
  )
151
+ return ""
152
+
153
+
154
+ ## Why [param child_name] cannot be given to a child of [param parent], or "". Godot renames a node
155
+ ## whose name is taken or holds a character a path cannot, and the next call naming it would then
156
+ ## reach another node.
157
+ func unusable_name(parent: Node, child_name: String, moving: Node = null) -> String:
158
+ var unfit: String = unfit_name(child_name)
159
+ if not unfit.is_empty():
160
+ return unfit
142
161
  var taken: Node = parent.get_node_or_null(NodePath(child_name))
143
162
  if taken != null and taken != moving and taken.get_parent() == parent:
144
163
  return "%s already has a child named %s." % [path_of(parent), child_name]
@@ -540,6 +540,9 @@ func _serialize_colors(value: PackedColorArray) -> Array:
540
540
 
541
541
  # A pathless Resource says so rather than inventing an empty path for the caller to load.
542
542
  func _serialize_object(value: Object) -> Variant:
543
+ if value is InputEvent:
544
+ var event: InputEvent = value
545
+ return _describe_input_event(event)
543
546
  if value is Resource:
544
547
  var resource: Resource = value
545
548
  if resource.resource_path.is_empty():
@@ -552,6 +555,23 @@ func _serialize_object(value: Object) -> Variant:
552
555
  return _describe_object(value)
553
556
 
554
557
 
558
+ ## An input event, by what it is bound to: the engine's own words for it and every field it stores
559
+ ## away from its default. As a bare resource it was its class and nothing else, so an input action
560
+ ## read back from project.godot could not say whether it was bound to Space or to a joypad button.
561
+ func _describe_input_event(event: InputEvent) -> Dictionary:
562
+ var described: Dictionary = {"class": event.get_class(), "_type": "Resource", "text": event.as_text()}
563
+ for property: Dictionary in event.get_property_list():
564
+ var usage: int = property.get("usage", 0)
565
+ var name: String = str(property.get("name", ""))
566
+ if not (usage & PROPERTY_USAGE_STORAGE) or name.begins_with("resource_") or name == "script":
567
+ continue
568
+ var held: Variant = event.get(name)
569
+ if held == ClassDB.class_get_property_default_value(event.get_class(), name):
570
+ continue
571
+ described[name] = serialize_value(held)
572
+ return described
573
+
574
+
555
575
  ## A plain object, said in enough detail to tell it from the one beside it.
556
576
  ##
557
577
  ## `{"class": "RefCounted"}` was all of it, so a roster of twelve people came back as the word
@@ -104,12 +104,12 @@ func _init_tools() -> void:
104
104
  # Resource tools
105
105
  "create_resource": [_resource_tools, "create_resource"],
106
106
  "modify_resource": [_resource_tools, "modify_resource"],
107
+ "read_resource": [_resource_tools, "read_resource"],
107
108
  "create_shader": [_resource_tools, "create_shader"],
108
109
  "create_tileset": [_resource_tools, "create_tileset"],
109
110
  "set_tilemap_cells": [_resource_tools, "set_tilemap_cells"],
110
111
  "set_theme_color": [_resource_tools, "set_theme_color"],
111
112
  "set_theme_font_size": [_resource_tools, "set_theme_font_size"],
112
- # Animation tools
113
113
  "play_scene": [_play_tools, "play_scene"],
114
114
  "restart_editor": [_play_tools, "restart_editor"],
115
115
  "quit_editor": [_play_tools, "quit_editor"],
@@ -118,6 +118,9 @@ func _init_tools() -> void:
118
118
  # The script profiler on a game the editor plays
119
119
  "profile_start": [_profile_tools, "profile_start"],
120
120
  "profile_read": [_profile_tools, "profile_read"],
121
+ # Animation tools
122
+ "read_animations": [_animation_tools, "read_animations"],
123
+ "read_animation_states": [_animation_tools, "read_animation_states"],
121
124
  "create_animation": [_animation_tools, "create_animation"],
122
125
  "add_animation_track": [_animation_tools, "add_animation_track"],
123
126
  "add_animation_state": [_animation_tools, "add_animation_state"],
@@ -6,9 +6,33 @@ extends Node
6
6
  const PropertyValues = preload("../property_values.gd")
7
7
  const Read = preload("../reading.gd")
8
8
  const SceneFile = preload("../scene_file.gd")
9
+ const Serialisation = preload("../serialisation.gd")
10
+
11
+ const LOOP_MODE_NAMES: Dictionary[int, String] = {
12
+ Animation.LOOP_NONE: "none",
13
+ Animation.LOOP_LINEAR: "linear",
14
+ Animation.LOOP_PINGPONG: "pingpong",
15
+ }
16
+ const TRACK_TYPE_NAMES: Dictionary[int, String] = {
17
+ Animation.TYPE_VALUE: "property",
18
+ Animation.TYPE_POSITION_3D: "position_3d",
19
+ Animation.TYPE_ROTATION_3D: "rotation_3d",
20
+ Animation.TYPE_SCALE_3D: "scale_3d",
21
+ Animation.TYPE_BLEND_SHAPE: "blend_shape",
22
+ Animation.TYPE_METHOD: "method",
23
+ Animation.TYPE_BEZIER: "bezier",
24
+ Animation.TYPE_AUDIO: "audio",
25
+ Animation.TYPE_ANIMATION: "animation",
26
+ }
27
+ const SWITCH_MODE_NAMES: Dictionary[int, String] = {
28
+ AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE: "immediate",
29
+ AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC: "sync",
30
+ AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END: "at_end",
31
+ }
9
32
 
10
33
  var _editor_plugin: EditorPlugin = null
11
34
  var _properties: PropertyValues = PropertyValues.new()
35
+ var _values: Serialisation = Serialisation.new()
12
36
 
13
37
 
14
38
  func set_editor_plugin(plugin: EditorPlugin) -> void:
@@ -47,6 +71,19 @@ func _open_at(scene_path: String, node_path: String, kind: String) -> Array:
47
71
  return [scene, node, {}]
48
72
 
49
73
 
74
+ ## [param scene_path] opened for reading, as [method _open_at] answers. A node an instanced scene
75
+ ## owns is read like any other, since nothing is written.
76
+ func _read_at(scene_path: String, node_path: String, kind: String) -> Array:
77
+ var scene: SceneFile = SceneFile.new()
78
+ var refused: Dictionary = scene.open(scene_path, false)
79
+ if not refused.is_empty():
80
+ return [scene, null, refused]
81
+ var node: Node = _find_node(scene.root, node_path)
82
+ if node == null or not node.is_class(kind):
83
+ return [scene, null, scene.refuse("%s not found at: %s" % [kind, node_path])]
84
+ return [scene, node, {}]
85
+
86
+
50
87
  ## Writes [param scene] once [param check] passes, then the files of [param homes], answering
51
88
  ## [param answer] or a refusal.
52
89
  func _finish(scene: SceneFile, check: Callable, homes: Dictionary, answer: Dictionary) -> Dictionary:
@@ -181,6 +218,141 @@ func _animated(player: AnimationPlayer, track_path: String) -> Node:
181
218
  return from.get_node_or_null(NodePath(track_path))
182
219
 
183
220
 
221
+ # =============================================================================
222
+ # read_animations, read_animation_states
223
+ # =============================================================================
224
+ ## The animations of an AnimationPlayer, or one of them whole. A track comes back in the shape
225
+ ## add_track takes, so one read here can be written to another animation as it stands.
226
+ func read_animations(args: Dictionary) -> Dictionary:
227
+ var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
228
+ var player_node_path: String = str(args.get("playerNodePath", "."))
229
+ var animation_name: String = str(args.get("animationName", ""))
230
+ if scene_path.strip_edges() == "res://":
231
+ return {"ok": false, "error": "Missing scenePath"}
232
+
233
+ var opened: Array = _read_at(scene_path, player_node_path, "AnimationPlayer")
234
+ var scene: SceneFile = opened[0]
235
+ if opened[1] == null:
236
+ return opened[2]
237
+ var player: AnimationPlayer = opened[1]
238
+ var answer: Dictionary = {"ok": true, "unsavedInEditor": SceneFile.unsaved(scene_path)}
239
+ if animation_name.is_empty():
240
+ var listed: Array[Dictionary] = []
241
+ for called: String in player.get_animation_list():
242
+ var listed_one: Animation = player.get_animation(StringName(called))
243
+ var summary: Dictionary = _summary(listed_one)
244
+ summary["animationName"] = called
245
+ summary["tracks"] = listed_one.get_track_count()
246
+ listed.append(summary)
247
+ answer["animations"] = listed
248
+ scene.close()
249
+ return answer
250
+ if not player.has_animation(StringName(animation_name)):
251
+ var names: PackedStringArray = player.get_animation_list()
252
+ return scene.refuse(
253
+ (
254
+ "%s has no animation %s. It has: %s."
255
+ % [player.name, animation_name, ", ".join(names) if not names.is_empty() else "none"]
256
+ )
257
+ )
258
+
259
+ var anim: Animation = player.get_animation(StringName(animation_name))
260
+ answer.merge(_summary(anim))
261
+ answer["animationName"] = animation_name
262
+ var tracks: Array[Dictionary] = []
263
+ for track: int in anim.get_track_count():
264
+ tracks.append(_track(anim, track))
265
+ answer["tracks"] = tracks
266
+ scene.close()
267
+ return answer
268
+
269
+
270
+ func _summary(anim: Animation) -> Dictionary:
271
+ return {
272
+ "length": anim.length,
273
+ "loopMode": LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
274
+ "step": anim.step,
275
+ }
276
+
277
+
278
+ func _track(anim: Animation, track: int) -> Dictionary:
279
+ var kind: int = anim.track_get_type(track)
280
+ var path: NodePath = anim.track_get_path(track)
281
+ var shaped: Dictionary = {
282
+ "type": TRACK_TYPE_NAMES.get(kind, str(kind)), "nodePath": str(path.get_concatenated_names())
283
+ }
284
+ var property: String = str(path.get_concatenated_subnames())
285
+ if not property.is_empty():
286
+ shaped["property"] = property
287
+ var keyframes: Array[Dictionary] = []
288
+ var methods: Dictionary = {}
289
+ for key: int in anim.track_get_key_count(track):
290
+ var keyframe: Dictionary = {"time": anim.track_get_key_time(track, key)}
291
+ if kind == Animation.TYPE_METHOD:
292
+ var method: String = str(anim.method_track_get_name(track, key))
293
+ methods[method] = true
294
+ keyframe["method"] = method
295
+ keyframe["args"] = _values.serialize_value(anim.method_track_get_params(track, key))
296
+ else:
297
+ keyframe["value"] = _values.serialize_value(anim.track_get_key_value(track, key))
298
+ keyframes.append(keyframe)
299
+ # One method across every key is the track's, as add_track takes it. Keys calling different
300
+ # methods, which the editor can make and add_track cannot, keep their own.
301
+ if methods.size() == 1:
302
+ shaped["method"] = methods.keys()[0]
303
+ for keyframe: Dictionary in keyframes:
304
+ var _shared: bool = keyframe.erase("method")
305
+ shaped["keyframes"] = keyframes
306
+ return shaped
307
+
308
+
309
+ ## The states and transitions of a state machine in an AnimationTree.
310
+ func read_animation_states(args: Dictionary) -> Dictionary:
311
+ var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
312
+ var anim_tree_path: String = str(args.get("animTreePath", ""))
313
+ var state_machine_path: String = str(args.get("stateMachinePath", ""))
314
+ if scene_path.strip_edges() == "res://":
315
+ return {"ok": false, "error": "Missing scenePath"}
316
+ if anim_tree_path.is_empty():
317
+ return {"ok": false, "error": "Missing animTreePath"}
318
+
319
+ var opened: Array = _read_at(scene_path, anim_tree_path, "AnimationTree")
320
+ var scene: SceneFile = opened[0]
321
+ if opened[1] == null:
322
+ return opened[2]
323
+ var anim_tree: AnimationTree = opened[1]
324
+ var sm: AnimationNodeStateMachine = _get_state_machine(anim_tree, state_machine_path)
325
+ if sm == null:
326
+ return scene.refuse("AnimationNodeStateMachine not found")
327
+
328
+ var states: Array[Dictionary] = []
329
+ for state: StringName in sm.get_node_list():
330
+ var node: AnimationNode = sm.get_node(state)
331
+ var entry: Dictionary = {"stateName": str(state), "type": node.get_class()}
332
+ if node is AnimationNodeAnimation:
333
+ var plays: AnimationNodeAnimation = node
334
+ entry["animationName"] = str(plays.animation)
335
+ states.append(entry)
336
+ var transitions: Array[Dictionary] = []
337
+ for index: int in sm.get_transition_count():
338
+ var transition: AnimationNodeStateMachineTransition = sm.get_transition(index)
339
+ var shaped: Dictionary = {
340
+ "from": str(sm.get_transition_from(index)),
341
+ "to": str(sm.get_transition_to(index)),
342
+ "transitionType": SWITCH_MODE_NAMES.get(transition.switch_mode, str(transition.switch_mode)),
343
+ }
344
+ if not str(transition.advance_condition).is_empty():
345
+ shaped["advanceCondition"] = str(transition.advance_condition)
346
+ transitions.append(shaped)
347
+ scene.close()
348
+ return {
349
+ "ok": true,
350
+ "states": states,
351
+ "transitions": transitions,
352
+ "unsavedInEditor": SceneFile.unsaved(scene_path),
353
+ }
354
+
355
+
184
356
  # =============================================================================
185
357
  # create_animation
186
358
  # =============================================================================
@@ -6,51 +6,21 @@ extends Node
6
6
  const PropertyValues = preload("../property_values.gd")
7
7
  const Read = preload("../reading.gd")
8
8
  const SceneFile = preload("../scene_file.gd")
9
+ const Serialisation = preload("../serialisation.gd")
9
10
 
10
- # Shader templates. Written as real multi-line source rather than escaped one-liners so that
11
- # what ends up in the .gdshader can be read here. The %s is the shader type.
11
+ # The minimal shader of a type: its declaration and the one function the engine calls for it.
12
12
  const SHADER_EMPTY: String = """shader_type %s;
13
13
 
14
- void fragment() {
14
+ void %s() {
15
15
  }
16
16
  """
17
-
18
- const SHADER_BASIC: String = """shader_type %s;
19
-
20
- void fragment() {
21
- COLOR = vec4(1.0);
22
- }
23
- """
24
-
25
- const SHADER_COLOR_SHIFT: String = """shader_type %s;
26
-
27
- uniform vec4 color_shift : source_color = vec4(0.1, 0.0, 0.2, 0.0);
28
-
29
- void fragment() {
30
- vec4 base = texture(TEXTURE, UV);
31
- COLOR = vec4(clamp(base.rgb + color_shift.rgb, 0.0, 1.0), base.a);
17
+ const SHADER_ENTRY: Dictionary[String, String] = {
18
+ "canvas_item": "fragment", "spatial": "fragment", "particles": "process", "sky": "sky", "fog": "fog"
32
19
  }
33
- """
34
-
35
- const SHADER_OUTLINE: String = """shader_type %s;
36
-
37
- uniform vec4 outline_color : source_color = vec4(0.0, 0.0, 0.0, 1.0);
38
- uniform float outline_width : hint_range(0.0, 8.0) = 1.0;
39
-
40
- void fragment() {
41
- vec2 px = TEXTURE_PIXEL_SIZE * outline_width;
42
- float a = texture(TEXTURE, UV).a;
43
- float edge = max(
44
- max(texture(TEXTURE, UV + vec2(px.x, 0.0)).a, texture(TEXTURE, UV - vec2(px.x, 0.0)).a),
45
- max(texture(TEXTURE, UV + vec2(0.0, px.y)).a, texture(TEXTURE, UV - vec2(0.0, px.y)).a)
46
- );
47
- vec4 base = texture(TEXTURE, UV);
48
- COLOR = mix(outline_color * edge, base, a);
49
- }
50
- """
51
20
 
52
21
  var _editor_plugin: EditorPlugin = null
53
22
  var _properties: PropertyValues = PropertyValues.new()
23
+ var _values: Serialisation = Serialisation.new()
54
24
 
55
25
 
56
26
  func set_editor_plugin(plugin: EditorPlugin) -> void:
@@ -217,6 +187,9 @@ func create_resource(args: Dictionary) -> Dictionary:
217
187
  if not refused.is_empty():
218
188
  return {"ok": false, "error": refused}
219
189
 
190
+ var folder: String = SceneFile.make_folder_for(res_path)
191
+ if not folder.is_empty():
192
+ return {"ok": false, "error": folder}
220
193
  var failed: String = _save_checked(resource, res_path, properties.keys(), null)
221
194
  if not failed.is_empty():
222
195
  return {"ok": false, "error": failed}
@@ -260,6 +233,50 @@ func modify_resource(args: Dictionary) -> Dictionary:
260
233
  return {"ok": true, "resourcePath": res_path}
261
234
 
262
235
 
236
+ ## What a resource file holds, in the form modify takes, so a value read here can be written back.
237
+ ## Read from the file rather than the editor's cached copy, since the file is what the other ops
238
+ ## write and what the project loads.
239
+ func read_resource(args: Dictionary) -> Dictionary:
240
+ var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
241
+ var include_defaults: bool = Read.as_bool(args.get("includeDefaults", false))
242
+ if res_path == "res://":
243
+ return {"ok": false, "error": "resourcePath is required"}
244
+ if not FileAccess.file_exists(res_path):
245
+ return {"ok": false, "error": "No resource at " + res_path}
246
+ var resource: Resource = ResourceLoader.load(res_path, "", ResourceLoader.CACHE_MODE_IGNORE)
247
+ if resource == null:
248
+ return {"ok": false, "error": "%s did not load as a resource." % res_path}
249
+
250
+ var script: Script = resource.get_script()
251
+ var scripted: Dictionary = {}
252
+ if script != null:
253
+ for declared: Dictionary in script.get_script_property_list():
254
+ scripted[str(declared.get("name", ""))] = true
255
+ var properties: Dictionary = {}
256
+ for p: Dictionary in resource.get_property_list():
257
+ var property: String = str(p.get("name", ""))
258
+ if property.is_empty() or property == "script":
259
+ continue
260
+ if not (Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
261
+ continue
262
+ var value: Variant = resource.get(property)
263
+ if not include_defaults:
264
+ var default: Variant = (
265
+ script.get_property_default_value(property)
266
+ if scripted.has(property)
267
+ else ClassDB.class_get_property_default_value(resource.get_class(), property)
268
+ )
269
+ if PropertyValues.same(value, default):
270
+ continue
271
+ properties[property] = _values.serialize_value(value)
272
+ var answer: Dictionary = {
273
+ "ok": true, "resourcePath": res_path, "resourceType": resource.get_class(), "properties": properties
274
+ }
275
+ if script != null:
276
+ answer["script"] = script.resource_path
277
+ return answer
278
+
279
+
263
280
  func create_shader(args: Dictionary) -> Dictionary:
264
281
  var shader_path: String = _ensure_res_path(str(args.get("shaderPath", "")))
265
282
  var shader_type: String = str(args.get("shaderType", "canvas_item"))
@@ -270,19 +287,12 @@ func create_shader(args: Dictionary) -> Dictionary:
270
287
  return {"ok": false, "error": taken}
271
288
 
272
289
  var code: String = str(args.get("code", ""))
273
- var template: String = str(args.get("template", ""))
274
-
275
290
  if code == "":
276
- match template:
277
- "basic":
278
- code = SHADER_BASIC % shader_type
279
- "color_shift":
280
- code = SHADER_COLOR_SHIFT % shader_type
281
- "outline":
282
- code = SHADER_OUTLINE % shader_type
283
- _:
284
- code = SHADER_EMPTY % shader_type
291
+ code = SHADER_EMPTY % [shader_type, SHADER_ENTRY.get(shader_type, "fragment")]
285
292
 
293
+ var folder: String = SceneFile.make_folder_for(shader_path)
294
+ if not folder.is_empty():
295
+ return {"ok": false, "error": folder}
286
296
  var file: FileAccess = FileAccess.open(shader_path, FileAccess.WRITE)
287
297
  if file == null:
288
298
  return {
@@ -335,6 +345,9 @@ func create_tileset(args: Dictionary) -> Dictionary:
335
345
  {"sourceId": added, "texture": atlas.texture.resource_path, "tiles": atlas.get_tiles_count()}
336
346
  )
337
347
 
348
+ var folder: String = SceneFile.make_folder_for(tileset_path)
349
+ if not folder.is_empty():
350
+ return {"ok": false, "error": folder}
338
351
  var saving: Error = ResourceSaver.save(tileset, tileset_path)
339
352
  if saving != OK:
340
353
  return {"ok": false, "error": "Failed to save %s: %s" % [tileset_path, error_string(saving)]}
@@ -8,6 +8,16 @@ const PropertyValues = preload("../property_values.gd")
8
8
  const SceneFile = preload("../scene_file.gd")
9
9
  const Serialisation = preload("../serialisation.gd")
10
10
 
11
+ ## The names Object.ConnectFlags gives each bit. The number alone left a reader guessing that 2 is
12
+ ## the persistence every connection in a saved scene carries.
13
+ const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
14
+ Object.CONNECT_DEFERRED: "deferred",
15
+ Object.CONNECT_PERSIST: "persist",
16
+ Object.CONNECT_ONE_SHOT: "one_shot",
17
+ Object.CONNECT_REFERENCE_COUNTED: "reference_counted",
18
+ Object.CONNECT_APPEND_SOURCE_OBJECT: "append_source_object",
19
+ }
20
+
11
21
  var _editor_plugin: EditorPlugin = null
12
22
  var _values: Serialisation = Serialisation.new()
13
23
  var _properties: PropertyValues = PropertyValues.new()
@@ -17,6 +27,19 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
17
27
  _editor_plugin = plugin
18
28
 
19
29
 
30
+ ## Each bit set in [param flags] by name, and whatever bits are left unnamed as one number.
31
+ static func _flag_names(flags: int) -> Array[String]:
32
+ var names: Array[String] = []
33
+ var rest: int = flags
34
+ for bit: int in CONNECT_FLAG_NAMES:
35
+ if flags & bit:
36
+ names.append(CONNECT_FLAG_NAMES[bit])
37
+ rest &= ~bit
38
+ if rest != 0:
39
+ names.append(str(rest))
40
+ return names
41
+
42
+
20
43
  func _ensure_res_path(path: String) -> String:
21
44
  if not path.begins_with("res://"):
22
45
  return "res://" + path
@@ -249,7 +272,8 @@ func create_scene(args: Dictionary) -> Dictionary:
249
272
  var project_path: String = str(args.get("projectPath", ""))
250
273
  var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
251
274
  var root_node_type: String = str(args.get("rootNodeType", "Node"))
252
- var script_path: String = str(args.get("scriptPath", ""))
275
+ var root_node_name: String = str(args.get("rootNodeName", root_node_type))
276
+ var script_path: String = str(args.get("script", ""))
253
277
 
254
278
  if scene_path == "res://":
255
279
  return {"ok": false, "error": "Missing scenePath"}
@@ -263,6 +287,9 @@ func create_scene(args: Dictionary) -> Dictionary:
263
287
  }
264
288
  if not ClassDB.can_instantiate(root_node_type):
265
289
  return {"ok": false, "error": "%s cannot be instantiated." % root_node_type}
290
+ var unfit: String = SceneFile.unfit_name(root_node_name)
291
+ if not unfit.is_empty():
292
+ return {"ok": false, "error": unfit}
266
293
  # Saving over a scene that is there replaced it, nodes and all, and answered as a new scene.
267
294
  if FileAccess.file_exists(scene_path):
268
295
  return {
@@ -278,7 +305,7 @@ func create_scene(args: Dictionary) -> Dictionary:
278
305
  }
279
306
 
280
307
  var root: Node = ClassDB.instantiate(root_node_type)
281
- root.name = root_node_type
308
+ root.name = root_node_name
282
309
 
283
310
  if not script_path.is_empty():
284
311
  var full_script_path: String = _to_scene_res_path(project_path, script_path)
@@ -296,6 +323,10 @@ func create_scene(args: Dictionary) -> Dictionary:
296
323
  }
297
324
  root.set_script(script)
298
325
 
326
+ var folder: String = SceneFile.make_folder_for(scene_path)
327
+ if not folder.is_empty():
328
+ root.free()
329
+ return {"ok": false, "error": folder}
299
330
  var scene: SceneFile = SceneFile.new()
300
331
  scene.root = root
301
332
  scene.path = scene_path
@@ -303,7 +334,9 @@ func create_scene(args: Dictionary) -> Dictionary:
303
334
  if not written.is_empty():
304
335
  return written
305
336
 
306
- return {"ok": true, "scenePath": scene_path, "rootNodeType": root_node_type}
337
+ return {
338
+ "ok": true, "scenePath": scene_path, "rootNodeType": root_node_type, "rootNodeName": root_node_name
339
+ }
307
340
 
308
341
 
309
342
  func list_scene_nodes(args: Dictionary) -> Dictionary:
@@ -597,12 +630,19 @@ func get_node_properties(args: Dictionary) -> Dictionary:
597
630
  # names this scene's file sets are always answered.
598
631
  var props: Dictionary = _properties_of(node, include_defaults, scene.set_here(node))
599
632
  scene.close()
600
- return {
633
+ var answer: Dictionary = {
601
634
  "ok": true,
602
635
  "nodePath": node_path,
603
636
  "properties": props,
604
637
  "unsavedInEditor": SceneFile.unsaved(scene_path)
605
638
  }
639
+ # An empty answer read as a node with nothing on it, rather than one with nothing changed.
640
+ if props.is_empty() and not include_defaults:
641
+ answer["note"] = (
642
+ "Every property of this node is at its default, so none is listed; "
643
+ + "includeDefaults: true lists them all."
644
+ )
645
+ return answer
606
646
 
607
647
 
608
648
  func save_scene(args: Dictionary) -> Dictionary:
@@ -739,6 +779,7 @@ func connect_signal(args: Dictionary) -> Dictionary:
739
779
  "targetNodePath": to,
740
780
  "methodName": method_name,
741
781
  "flags": saved_flags[0],
782
+ "flagNames": _flag_names(Read.as_int(saved_flags[0])),
742
783
  # Connected either way, as the editor's own dialog allows, since the method may be written
743
784
  # next; a caller told nothing learnt of a misspelt one only when the signal fired.
744
785
  "methodFound": method_found
@@ -894,6 +935,7 @@ func list_connections(args: Dictionary) -> Dictionary:
894
935
  "targetNodePath": target_path,
895
936
  "methodName": str(callable.get_method()),
896
937
  "flags": flags,
938
+ "flagNames": _flag_names(flags),
897
939
  # False for a connection an instanced scene makes inside itself, which only that
898
940
  # scene can change.
899
941
  "editable": editable
@@ -250,14 +250,24 @@ func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false
250
250
  # texture keeps whatever was drawn last. A capture then comes back byte for byte the same
251
251
  # every time, with a success payload, and a game running perfectly well reads as a game
252
252
  # that has frozen. A frame nobody drew is not evidence of anything, so it is refused.
253
+ # Told apart, because the remedies are: a headless game told to restore its window had none.
254
+ if DisplayServer.get_name() == "headless":
255
+ return {
256
+ "type": "error",
257
+ "message":
258
+ (
259
+ "This game runs headless, so it has no window and draws nothing to capture. Start it "
260
+ + "with a window (editor_run start with headless false) to capture it."
261
+ ),
262
+ }
253
263
  if not _host.get_tree().root.can_draw():
254
264
  return {
255
265
  "type": "error",
256
266
  "message":
257
267
  (
258
- "Nothing is being drawn to the game's window: it is minimised, or this engine "
259
- + "has no window. The texture still holds the last frame that was drawn, so this "
260
- + "and every capture after it would be that frame. Restore the window and ask again."
268
+ "Nothing is being drawn to the game's window because it is minimised. The texture "
269
+ + "still holds the last frame that was drawn, so this and every capture after it "
270
+ + "would be that frame. Restore the window and ask again."
261
271
  ),
262
272
  }
263
273