gdharness 1.2.36 → 1.2.38
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/build/cli.js +918 -439
- package/build/godot/addons/gdharness_editor/headless_check.gd +411 -0
- package/build/godot/addons/gdharness_editor/headless_check.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +3 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +408 -10
- package/build/godot/addons/gdharness_editor/serialisation.gd +103 -5
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +88 -25
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +68 -19
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +9 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +3 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +103 -5
- package/build/godot/operations/editor_tools.gd +57 -0
- package/build/godot/operations/gdscript_analysis.gd +26 -2
- package/build/godot/operations/gdscript_authoring.gd +5 -2
- package/build/godot/operations/godot_operations.gd +58 -1
- package/build/godot/operations/property_values.gd +3 -0
- package/build/godot/operations/serialisation.gd +103 -5
- package/build/index.js +838 -366
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ extends Node
|
|
|
4
4
|
## Resource files, shaders, tilesets and themes, written through the open editor.
|
|
5
5
|
|
|
6
6
|
const FileReads = preload("../file_reads.gd")
|
|
7
|
+
const HeadlessCheck = preload("../headless_check.gd")
|
|
7
8
|
const PropertyValues = preload("../property_values.gd")
|
|
8
9
|
const Read = preload("../reading.gd")
|
|
9
10
|
const SceneFile = preload("../scene_file.gd")
|
|
@@ -118,9 +119,12 @@ func _load_theme(theme_path: String) -> Theme:
|
|
|
118
119
|
## Read back because a property can be set and still not be saved: one without storage, a
|
|
119
120
|
## script variable that is not exported. Both were answered as changed.
|
|
120
121
|
func _save_checked(resource: Resource, path: String, names: Array, previous: Variant) -> String:
|
|
121
|
-
var saving: Error =
|
|
122
|
+
var saving: Error = SceneFile.save_keeping_uid(resource, path)
|
|
122
123
|
if saving != OK:
|
|
123
124
|
return "Failed to save %s: %s" % [path, error_string(saving)]
|
|
125
|
+
var unkept: String = _unkept_headless(resource, path, names, previous)
|
|
126
|
+
if not unkept.is_empty():
|
|
127
|
+
return "Nothing was written: " + unkept + HeadlessCheck.put_back(path, previous)
|
|
124
128
|
var loaded: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
125
129
|
var lost: String = ""
|
|
126
130
|
if loaded == null:
|
|
@@ -141,36 +145,85 @@ func _save_checked(resource: Resource, path: String, names: Array, previous: Var
|
|
|
141
145
|
if lost.is_empty():
|
|
142
146
|
_written(path)
|
|
143
147
|
return ""
|
|
148
|
+
return lost + HeadlessCheck.put_back(path, previous)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Outside the editor, puts back in the file just saved at [param path] what a script that is not a
|
|
152
|
+
## tool changed and the call did not ask for, [param names] being what it asked for, and answers ""
|
|
153
|
+
## or why that did not hold. See [HeadlessCheck].
|
|
154
|
+
func _unkept_headless(resource: Resource, path: String, names: Array, previous: Variant) -> String:
|
|
155
|
+
if Engine.is_editor_hint():
|
|
156
|
+
return ""
|
|
157
|
+
var script: Script = resource.get_script()
|
|
158
|
+
var scripted: bool = script != null and not script.is_tool()
|
|
159
|
+
if not path.ends_with(".tres"):
|
|
160
|
+
if scripted or HeadlessCheck.holds_scripted_embedded(resource, {}):
|
|
161
|
+
return (
|
|
162
|
+
(
|
|
163
|
+
(
|
|
164
|
+
"%s is a binary resource with a script that is not a tool, and what that script "
|
|
165
|
+
+ "changed when a headless engine loaded it cannot be told apart from the file here. "
|
|
166
|
+
)
|
|
167
|
+
% path
|
|
168
|
+
)
|
|
169
|
+
+ HeadlessCheck.REMEDY
|
|
170
|
+
)
|
|
171
|
+
return ""
|
|
172
|
+
var before: String = ""
|
|
144
173
|
if previous is PackedByteArray:
|
|
145
174
|
var bytes: PackedByteArray = previous
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
175
|
+
before = bytes.get_string_from_utf8()
|
|
176
|
+
var asked: Dictionary = {"script": true}
|
|
177
|
+
var reached: Dictionary = {}
|
|
178
|
+
for key: Variant in names:
|
|
179
|
+
var property: String = str(key)
|
|
180
|
+
asked[property] = true
|
|
181
|
+
asked[property.get_slice(":", 0)] = true
|
|
182
|
+
reached.merge(HeadlessCheck.reached(resource, property))
|
|
183
|
+
return HeadlessCheck.settle_saved(
|
|
184
|
+
path,
|
|
185
|
+
before,
|
|
186
|
+
HeadlessCheck.stored(resource).values(),
|
|
187
|
+
resource if scripted else null,
|
|
188
|
+
asked,
|
|
189
|
+
reached,
|
|
190
|
+
func() -> Error: return SceneFile.save_keeping_uid(resource, path)
|
|
191
|
+
)
|
|
152
192
|
|
|
153
193
|
|
|
154
194
|
func create_resource(args: Dictionary) -> Dictionary:
|
|
155
195
|
var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
|
|
156
196
|
var resource_type: String = str(args.get("resourceType", "Resource"))
|
|
197
|
+
var asked_type: String = resource_type
|
|
157
198
|
if res_path == "res://":
|
|
158
199
|
return {"ok": false, "error": "resourcePath is required"}
|
|
159
200
|
var taken: String = _taken(res_path, "resource_edit op=create")
|
|
160
201
|
if not taken.is_empty():
|
|
161
202
|
return {"ok": false, "error": taken}
|
|
162
203
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
):
|
|
204
|
+
var script_path: String = str(args.get("script", ""))
|
|
205
|
+
var made: Dictionary = SceneFile.made_as(
|
|
206
|
+
resource_type, "" if script_path == "" else _ensure_res_path(script_path), "Resource", "resourceType"
|
|
207
|
+
)
|
|
208
|
+
if made.has("error"):
|
|
209
|
+
return {"ok": false, "error": made["error"]}
|
|
210
|
+
resource_type = str(made["type"])
|
|
211
|
+
script_path = str(made["script"])
|
|
212
|
+
if not ClassDB.class_exists(resource_type):
|
|
213
|
+
return {
|
|
214
|
+
"ok": false,
|
|
215
|
+
"error":
|
|
216
|
+
(
|
|
217
|
+
"%s is neither an engine class nor a class_name a script in this project declares."
|
|
218
|
+
% resource_type
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
if not ClassDB.is_parent_class(resource_type, "Resource") or not ClassDB.can_instantiate(resource_type):
|
|
168
222
|
return {"ok": false, "error": "%s is not a resource class that can be created." % resource_type}
|
|
169
223
|
var resource: Resource = ClassDB.instantiate(resource_type)
|
|
170
224
|
|
|
171
225
|
# A script that did not load was skipped, and a plain resource was saved in place of the custom
|
|
172
226
|
# one asked for.
|
|
173
|
-
var script_path: String = str(args.get("script", ""))
|
|
174
227
|
if script_path != "":
|
|
175
228
|
var script_file: String = _ensure_res_path(script_path)
|
|
176
229
|
var script: Script = load(script_file) as Script
|
|
@@ -198,7 +251,7 @@ func create_resource(args: Dictionary) -> Dictionary:
|
|
|
198
251
|
var failed: String = _save_checked(resource, res_path, properties.keys(), null)
|
|
199
252
|
if not failed.is_empty():
|
|
200
253
|
return {"ok": false, "error": failed}
|
|
201
|
-
return {"ok": true, "resourcePath": res_path, "resourceType":
|
|
254
|
+
return {"ok": true, "resourcePath": res_path, "resourceType": asked_type}
|
|
202
255
|
|
|
203
256
|
|
|
204
257
|
func modify_resource(args: Dictionary) -> Dictionary:
|
|
@@ -321,7 +374,7 @@ func create_tileset(args: Dictionary) -> Dictionary:
|
|
|
321
374
|
var folder: String = SceneFile.make_folder_for(tileset_path)
|
|
322
375
|
if not folder.is_empty():
|
|
323
376
|
return {"ok": false, "error": folder}
|
|
324
|
-
var saving: Error =
|
|
377
|
+
var saving: Error = SceneFile.save_keeping_uid(tileset, tileset_path)
|
|
325
378
|
if saving != OK:
|
|
326
379
|
return {"ok": false, "error": "Failed to save %s: %s" % [tileset_path, error_string(saving)]}
|
|
327
380
|
|
|
@@ -443,6 +496,7 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
|
|
|
443
496
|
# reports placing tiles and a scene that shows none.
|
|
444
497
|
var tile_set: TileSet = layered.tile_set if layered != null else single.tile_set
|
|
445
498
|
|
|
499
|
+
scene.asked(node, ["layer_%d/tile_data" % layer if layered != null else "tile_map_data"])
|
|
446
500
|
var wanted: Array = []
|
|
447
501
|
for index: int in placed.size():
|
|
448
502
|
var cell: Array = _cell(placed[index], index, tile_set)
|
|
@@ -621,6 +675,19 @@ func _cell_at(node: Node, layer: int, at: Vector2i) -> Array:
|
|
|
621
675
|
return []
|
|
622
676
|
|
|
623
677
|
|
|
678
|
+
## Saves [param theme] over [param theme_path] with [param item] changed, answering "" or why not.
|
|
679
|
+
func _save_theme(theme: Theme, theme_path: String, item: String) -> String:
|
|
680
|
+
var previous: PackedByteArray = FileAccess.get_file_as_bytes(theme_path)
|
|
681
|
+
var saving: Error = SceneFile.save_keeping_uid(theme, theme_path)
|
|
682
|
+
if saving != OK:
|
|
683
|
+
return "Failed to save %s: %s" % [theme_path, error_string(saving)]
|
|
684
|
+
var unkept: String = _unkept_headless(theme, theme_path, [item], previous)
|
|
685
|
+
if not unkept.is_empty():
|
|
686
|
+
return "Nothing was written: " + unkept + HeadlessCheck.put_back(theme_path, previous)
|
|
687
|
+
_written(theme_path)
|
|
688
|
+
return ""
|
|
689
|
+
|
|
690
|
+
|
|
624
691
|
func set_theme_color(args: Dictionary) -> Dictionary:
|
|
625
692
|
var theme_path: String = _ensure_res_path(str(args.get("themePath", "")))
|
|
626
693
|
if theme_path == "res://":
|
|
@@ -644,11 +711,9 @@ func set_theme_color(args: Dictionary) -> Dictionary:
|
|
|
644
711
|
)
|
|
645
712
|
theme.set_color(color_name, control_type, color)
|
|
646
713
|
|
|
647
|
-
var
|
|
648
|
-
if
|
|
649
|
-
return {"ok": false, "error":
|
|
650
|
-
|
|
651
|
-
_written(theme_path)
|
|
714
|
+
var unsaved: String = _save_theme(theme, theme_path, "%s/colors/%s" % [control_type, color_name])
|
|
715
|
+
if not unsaved.is_empty():
|
|
716
|
+
return {"ok": false, "error": unsaved}
|
|
652
717
|
|
|
653
718
|
var saved: Theme = _load_theme(theme_path)
|
|
654
719
|
if saved == null or not saved.has_color(color_name, control_type):
|
|
@@ -680,11 +745,9 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
|
|
|
680
745
|
|
|
681
746
|
theme.set_font_size(font_size_name, control_type, Read.as_int(args.get("size", 0)))
|
|
682
747
|
|
|
683
|
-
var
|
|
684
|
-
if
|
|
685
|
-
return {"ok": false, "error":
|
|
686
|
-
|
|
687
|
-
_written(theme_path)
|
|
748
|
+
var unsaved: String = _save_theme(theme, theme_path, "%s/font_sizes/%s" % [control_type, font_size_name])
|
|
749
|
+
if not unsaved.is_empty():
|
|
750
|
+
return {"ok": false, "error": unsaved}
|
|
688
751
|
|
|
689
752
|
var saved: Theme = _load_theme(theme_path)
|
|
690
753
|
if saved == null or not saved.has_font_size(font_size_name, control_type):
|
|
@@ -90,7 +90,7 @@ func _missing_on(node: Node, missing: String) -> String:
|
|
|
90
90
|
if probe.reload() == OK:
|
|
91
91
|
return missing
|
|
92
92
|
return (
|
|
93
|
-
"%s. Its script, %s, does not compile, so
|
|
93
|
+
"%s. Its script, %s, does not compile, so none of what it declares is known here."
|
|
94
94
|
% [missing, script.resource_path]
|
|
95
95
|
)
|
|
96
96
|
|
|
@@ -186,6 +186,17 @@ func create_scene(args: Dictionary) -> Dictionary:
|
|
|
186
186
|
return {"ok": false, "error": "Missing scenePath"}
|
|
187
187
|
if not scene_path.ends_with(".tscn"):
|
|
188
188
|
scene_path += ".tscn"
|
|
189
|
+
var asked_type: String = root_node_type
|
|
190
|
+
var made: Dictionary = SceneFile.made_as(
|
|
191
|
+
root_node_type,
|
|
192
|
+
"" if script_path.is_empty() else _to_scene_res_path(project_path, script_path),
|
|
193
|
+
"Node",
|
|
194
|
+
"rootNodeType"
|
|
195
|
+
)
|
|
196
|
+
if made.has("error"):
|
|
197
|
+
return {"ok": false, "error": made["error"]}
|
|
198
|
+
root_node_type = str(made["type"])
|
|
199
|
+
script_path = str(made["script"])
|
|
189
200
|
# A class that is not a node went into a Node variable, which is a runtime error the caller
|
|
190
201
|
# heard about only as "Invalid tool result".
|
|
191
202
|
if not ClassDB.class_exists(root_node_type) or not ClassDB.is_parent_class(root_node_type, "Node"):
|
|
@@ -213,6 +224,10 @@ func create_scene(args: Dictionary) -> Dictionary:
|
|
|
213
224
|
|
|
214
225
|
var root: Node = ClassDB.instantiate(root_node_type)
|
|
215
226
|
root.name = root_node_name
|
|
227
|
+
var scene: SceneFile = SceneFile.new()
|
|
228
|
+
scene.root = root
|
|
229
|
+
scene.path = scene_path
|
|
230
|
+
scene.asked(root, ["script"])
|
|
216
231
|
|
|
217
232
|
if not script_path.is_empty():
|
|
218
233
|
var full_script_path: String = _to_scene_res_path(project_path, script_path)
|
|
@@ -233,21 +248,19 @@ func create_scene(args: Dictionary) -> Dictionary:
|
|
|
233
248
|
"%s extends %s, so it cannot be on a %s root." % [full_script_path, base, root_node_type]
|
|
234
249
|
}
|
|
235
250
|
root.set_script(script)
|
|
251
|
+
# Named again, since a headless engine runs the script's _init as it is attached, and a node's
|
|
252
|
+
# name is no property the write compares.
|
|
253
|
+
root.name = root_node_name
|
|
236
254
|
|
|
237
255
|
var folder: String = SceneFile.make_folder_for(scene_path)
|
|
238
256
|
if not folder.is_empty():
|
|
239
257
|
root.free()
|
|
240
258
|
return {"ok": false, "error": folder}
|
|
241
|
-
var scene: SceneFile = SceneFile.new()
|
|
242
|
-
scene.root = root
|
|
243
|
-
scene.path = scene_path
|
|
244
259
|
var written: Dictionary = scene.write()
|
|
245
260
|
if not written.is_empty():
|
|
246
261
|
return written
|
|
247
262
|
|
|
248
|
-
return {
|
|
249
|
-
"ok": true, "scenePath": scene_path, "rootNodeType": root_node_type, "rootNodeName": root_node_name
|
|
250
|
-
}
|
|
263
|
+
return {"ok": true, "scenePath": scene_path, "rootNodeType": asked_type, "rootNodeName": root_node_name}
|
|
251
264
|
|
|
252
265
|
|
|
253
266
|
func list_scene_nodes(args: Dictionary) -> Dictionary:
|
|
@@ -275,6 +288,16 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
275
288
|
|
|
276
289
|
if node_name.is_empty():
|
|
277
290
|
return {"ok": false, "error": "Missing nodeName"}
|
|
291
|
+
var asked_type: String = node_type
|
|
292
|
+
var class_script: Script = null
|
|
293
|
+
if not node_type.is_empty() and not ClassDB.class_exists(node_type):
|
|
294
|
+
var declared: Dictionary = SceneFile.script_class(node_type, "Node")
|
|
295
|
+
if declared.has("error"):
|
|
296
|
+
return {"ok": false, "error": declared["error"]}
|
|
297
|
+
var held: Variant = declared.get("script")
|
|
298
|
+
if held is Script:
|
|
299
|
+
class_script = held
|
|
300
|
+
node_type = str(declared["base"])
|
|
278
301
|
var making: Array = Placing.what_to_add(node_type, instance_path, scene_path)
|
|
279
302
|
var packed: PackedScene = making[0]
|
|
280
303
|
if not str(making[1]).is_empty():
|
|
@@ -303,10 +326,17 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
303
326
|
|
|
304
327
|
# In the tree before its properties, so a property holding a node can find it by path.
|
|
305
328
|
var new_node: Node = Placing.made(packed, node_type)
|
|
329
|
+
if class_script != null:
|
|
330
|
+
scene.asked(new_node, ["script"])
|
|
331
|
+
new_node.set_script(class_script)
|
|
306
332
|
new_node.name = node_name
|
|
307
333
|
parent.add_child(new_node)
|
|
308
334
|
Placing.place(new_node, place)
|
|
309
335
|
Placing.own(new_node, root)
|
|
336
|
+
var unsettled: String = scene.settle()
|
|
337
|
+
if not unsettled.is_empty():
|
|
338
|
+
return scene.refuse("Nothing was written: " + unsettled)
|
|
339
|
+
scene.asked(new_node, properties.keys())
|
|
310
340
|
var refused_property: String = _properties.write_all(new_node, properties)
|
|
311
341
|
if not refused_property.is_empty():
|
|
312
342
|
return scene.refuse(refused_property)
|
|
@@ -316,12 +346,15 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
316
346
|
var answer: Dictionary = {
|
|
317
347
|
"ok": true,
|
|
318
348
|
"nodeName": node_name,
|
|
319
|
-
"nodeType": new_node.get_class(),
|
|
349
|
+
"nodeType": asked_type if class_script != null else new_node.get_class(),
|
|
320
350
|
"nodePath": new_path,
|
|
321
351
|
"index": new_node.get_index(),
|
|
322
352
|
}
|
|
323
353
|
var written: Dictionary = scene.write(
|
|
324
|
-
func(saved: Node) -> String:
|
|
354
|
+
func(saved: Node) -> String:
|
|
355
|
+
return _kept(
|
|
356
|
+
scene, saved, new_node, properties.keys() + (["script"] if class_script != null else [])
|
|
357
|
+
)
|
|
325
358
|
)
|
|
326
359
|
if not written.is_empty():
|
|
327
360
|
return written
|
|
@@ -337,7 +370,14 @@ func delete_node(args: Dictionary) -> Dictionary:
|
|
|
337
370
|
var node_path: String = str(args.get("nodePath", ""))
|
|
338
371
|
|
|
339
372
|
if node_path.is_empty() or node_path == ".":
|
|
340
|
-
return {
|
|
373
|
+
return {
|
|
374
|
+
"ok": false,
|
|
375
|
+
"error":
|
|
376
|
+
(
|
|
377
|
+
"The root cannot be deleted, since a scene has exactly one. Delete the nodes under it, or "
|
|
378
|
+
+ "make a scene with another root with scene_create."
|
|
379
|
+
)
|
|
380
|
+
}
|
|
341
381
|
|
|
342
382
|
var opened: Array = _open(scene_path, true)
|
|
343
383
|
var scene: SceneFile = opened[0]
|
|
@@ -415,6 +455,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
|
|
|
415
455
|
target_parent.add_child(duplicated_node)
|
|
416
456
|
Placing.own(duplicated_node, root)
|
|
417
457
|
Placing.reconnect(copies, source)
|
|
458
|
+
scene.copied(copies)
|
|
418
459
|
|
|
419
460
|
var new_path: String = scene.path_of(duplicated_node)
|
|
420
461
|
var written: Dictionary = scene.write(
|
|
@@ -474,6 +515,9 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
474
515
|
return scene.refuse(unplaceable)
|
|
475
516
|
|
|
476
517
|
var old_path: String = scene.path_of(node)
|
|
518
|
+
# Unowned while it moves: a node taken out of the tree keeps its owner, and adding it under the new
|
|
519
|
+
# parent warned that the owner was not above it. Node.reparent warns the same way.
|
|
520
|
+
node.owner = null
|
|
477
521
|
node.get_parent().remove_child(node)
|
|
478
522
|
new_parent.add_child(node)
|
|
479
523
|
Placing.place(node, place)
|
|
@@ -530,6 +574,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
530
574
|
if not unusable.is_empty():
|
|
531
575
|
return scene.refuse(unusable)
|
|
532
576
|
|
|
577
|
+
scene.asked(node, properties.keys())
|
|
533
578
|
var refused_property: String = _properties.write_all(node, properties)
|
|
534
579
|
if not refused_property.is_empty():
|
|
535
580
|
return scene.refuse(_missing_on(node, refused_property))
|
|
@@ -570,12 +615,10 @@ func get_node_properties(args: Dictionary) -> Dictionary:
|
|
|
570
615
|
var props: Dictionary = _properties_of(node, include_defaults, scene.set_here(node))
|
|
571
616
|
var cells: Dictionary = ResourceTools.cells_of(node)
|
|
572
617
|
scene.close()
|
|
573
|
-
var answer: Dictionary = {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
"
|
|
577
|
-
"unsavedInEditor": SceneFile.unsaved(scene_path)
|
|
578
|
-
}
|
|
618
|
+
var answer: Dictionary = {"ok": true, "nodePath": node_path, "properties": props}
|
|
619
|
+
# Only an editor holds anything unsaved; a headless engine answering false would say it looked.
|
|
620
|
+
if Engine.is_editor_hint():
|
|
621
|
+
answer["unsavedInEditor"] = SceneFile.unsaved(scene_path)
|
|
579
622
|
answer.merge(cells)
|
|
580
623
|
# An empty answer read as a node with nothing on it, rather than one with nothing changed.
|
|
581
624
|
if props.is_empty() and not include_defaults:
|
|
@@ -600,7 +643,10 @@ func save_scene(args: Dictionary) -> Dictionary:
|
|
|
600
643
|
return {"ok": false, "error": unsaved}
|
|
601
644
|
|
|
602
645
|
var open_root: Node = null
|
|
603
|
-
|
|
646
|
+
var open_roots: Array[Node] = []
|
|
647
|
+
if Engine.is_editor_hint():
|
|
648
|
+
open_roots = EditorInterface.get_open_scene_roots()
|
|
649
|
+
for open: Node in open_roots:
|
|
604
650
|
if open.scene_file_path == scene_path:
|
|
605
651
|
open_root = open
|
|
606
652
|
if open_root != null and target_path == scene_path:
|
|
@@ -618,7 +664,7 @@ func save_scene(args: Dictionary) -> Dictionary:
|
|
|
618
664
|
written = SceneFile.save_packed(packed, target_path)
|
|
619
665
|
else:
|
|
620
666
|
var scene: SceneFile = SceneFile.new()
|
|
621
|
-
var refused: Dictionary = scene.open(scene_path,
|
|
667
|
+
var refused: Dictionary = scene.open(scene_path, true)
|
|
622
668
|
if not refused.is_empty():
|
|
623
669
|
return refused
|
|
624
670
|
scene.path = target_path
|
|
@@ -897,4 +943,7 @@ func list_connections(args: Dictionary) -> Dictionary:
|
|
|
897
943
|
)
|
|
898
944
|
|
|
899
945
|
scene.close()
|
|
900
|
-
|
|
946
|
+
var listed: Dictionary = {"ok": true, "connections": connections}
|
|
947
|
+
if Engine.is_editor_hint():
|
|
948
|
+
listed["unsavedInEditor"] = SceneFile.unsaved(scene_path)
|
|
949
|
+
return listed
|
|
@@ -168,6 +168,12 @@ func _argument_for(
|
|
|
168
168
|
% [called, named, type_string(wants), index + 1, JSON.stringify(sent)]
|
|
169
169
|
)
|
|
170
170
|
}
|
|
171
|
+
var rounded: String = Values.inexact(sent, wants)
|
|
172
|
+
if not rounded.is_empty():
|
|
173
|
+
return {
|
|
174
|
+
"type": "error",
|
|
175
|
+
"message": "%s.%s takes int as argument %d: %s." % [called, named, index + 1, rounded]
|
|
176
|
+
}
|
|
171
177
|
var given: Variant = _values.fitted(sent, wants)
|
|
172
178
|
if not Values.acceptable(given, wants):
|
|
173
179
|
return {
|
|
@@ -242,6 +248,9 @@ func _value_for(
|
|
|
242
248
|
"message": "%s.%s holds an object: %s" % [called, named, named_object["message"]]
|
|
243
249
|
}
|
|
244
250
|
return {"value": named_object["object"]}
|
|
251
|
+
var rounded: String = Values.inexact(value, wanted)
|
|
252
|
+
if not rounded.is_empty():
|
|
253
|
+
return {"type": "error", "message": "%s.%s holds int: %s." % [called, named, rounded]}
|
|
245
254
|
var as_sent: Variant = _values.fitted(value, TYPE_NIL)
|
|
246
255
|
var cut: bool = Values.cuts(value, wanted)
|
|
247
256
|
var given: Variant = as_sent
|
|
@@ -156,6 +156,11 @@ static func cuts(given: Variant, type: int) -> bool:
|
|
|
156
156
|
return not (whole and (value == 0.0 or value == 1.0))
|
|
157
157
|
|
|
158
158
|
|
|
159
|
+
## Why [param given] may not be the whole number sent, or "": see [method Serialisation.inexact].
|
|
160
|
+
static func inexact(given: Variant, type: int) -> String:
|
|
161
|
+
return Serialisation.inexact(given, type)
|
|
162
|
+
|
|
163
|
+
|
|
159
164
|
## How a value of [param type] is written to be read as one, or empty when it is not built from fields.
|
|
160
165
|
static func written_as(type: int) -> String:
|
|
161
166
|
return Serialisation.written_as(type)
|
|
@@ -255,6 +255,9 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
255
255
|
var refused: String = _not_comparable(current, wanted, node_path, property)
|
|
256
256
|
if refused.is_empty():
|
|
257
257
|
refused = _cut_to_fit(params["value"], typeof(current), node_path, property)
|
|
258
|
+
var rounded: String = Values.inexact(params["value"], typeof(current))
|
|
259
|
+
if refused.is_empty() and not rounded.is_empty():
|
|
260
|
+
refused = "%s.%s holds int: %s." % [node_path, property, rounded]
|
|
258
261
|
var never_held: bool = typeof(wanted) != TYPE_NIL and typeof(wanted) != TYPE_OBJECT
|
|
259
262
|
if refused.is_empty() and declared == TYPE_OBJECT and never_held:
|
|
260
263
|
refused = (
|
|
@@ -16,6 +16,10 @@ const Read = preload("reading.gd")
|
|
|
16
16
|
const NUMBERS: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT]
|
|
17
17
|
const TEXTS: Array[int] = [TYPE_STRING, TYPE_STRING_NAME, TYPE_NODE_PATH]
|
|
18
18
|
|
|
19
|
+
# 2^53. A JSON number is a double, and every whole number below this one in size has a double of
|
|
20
|
+
# its own; from here on two or more share each double, so one arriving tells nobody which was sent.
|
|
21
|
+
const EXACT: int = 9007199254740992
|
|
22
|
+
|
|
19
23
|
# The types that cross as a plain JSON list and have to be built back into themselves. A list is
|
|
20
24
|
# what a caller can write by hand and what the serialiser answers with, and the parameter or
|
|
21
25
|
# property being written is the only thing that knows which of these it wanted.
|
|
@@ -53,6 +57,8 @@ const LIST_TYPES: Array[int] = [
|
|
|
53
57
|
# type it wanted.
|
|
54
58
|
const SERIALISERS: Dictionary = {
|
|
55
59
|
TYPE_NIL: "_serialize_nil",
|
|
60
|
+
TYPE_INT: "_serialize_int",
|
|
61
|
+
TYPE_PACKED_INT64_ARRAY: "_serialize_ints",
|
|
56
62
|
TYPE_FLOAT: "_serialize_float",
|
|
57
63
|
TYPE_PACKED_FLOAT32_ARRAY: "_serialize_floats",
|
|
58
64
|
TYPE_PACKED_FLOAT64_ARRAY: "_serialize_floats",
|
|
@@ -109,6 +115,8 @@ const BUILDERS: Dictionary = {
|
|
|
109
115
|
"NodePath": "_build_node_path",
|
|
110
116
|
"PackedByteArray": "_build_bytes",
|
|
111
117
|
"float": "_build_float",
|
|
118
|
+
"int": "_build_int",
|
|
119
|
+
"RID": "_build_rid",
|
|
112
120
|
"Dictionary": "_build_dictionary",
|
|
113
121
|
}
|
|
114
122
|
|
|
@@ -136,6 +144,8 @@ const REQUIRED: Dictionary = {
|
|
|
136
144
|
"NodePath": [["path"]],
|
|
137
145
|
"PackedByteArray": [["base64"]],
|
|
138
146
|
"float": [["value"]],
|
|
147
|
+
"int": [["value"]],
|
|
148
|
+
"RID": [["id"]],
|
|
139
149
|
"Dictionary": [["entries"]],
|
|
140
150
|
}
|
|
141
151
|
|
|
@@ -243,9 +253,40 @@ func deserialize_value(value: Variant) -> Variant:
|
|
|
243
253
|
return fields
|
|
244
254
|
if tag == "Dictionary" and not pairs(fields["entries"]):
|
|
245
255
|
return fields
|
|
256
|
+
# Digits only: the tag is there to carry what a number cannot, and {"value": 5} meant for some
|
|
257
|
+
# other shape is not to be read as 5.
|
|
258
|
+
if tag == "int" and not (fields["value"] is String and _exact_int(fields["value"]) != null):
|
|
259
|
+
return fields
|
|
260
|
+
if tag == "RID" and _exact_int(fields["id"]) == null:
|
|
261
|
+
return fields
|
|
246
262
|
return call(builder, fields)
|
|
247
263
|
|
|
248
264
|
|
|
265
|
+
## [param held] as the whole number it spells exactly, or null: an int, a JSON number that is whole
|
|
266
|
+
## and no larger than a double holds exactly, the digits of one, or the int tag carrying them. A
|
|
267
|
+
## fraction, or a number past 2^53 that arrived as a number and so may already be a neighbour of the
|
|
268
|
+
## one meant, is no id.
|
|
269
|
+
func _exact_int(held: Variant) -> Variant:
|
|
270
|
+
if held is int:
|
|
271
|
+
return held
|
|
272
|
+
if held is float:
|
|
273
|
+
var number: float = held
|
|
274
|
+
if is_finite(number) and number == floorf(number) and absf(number) < EXACT:
|
|
275
|
+
return int(number)
|
|
276
|
+
return null
|
|
277
|
+
if held is String:
|
|
278
|
+
var digits: String = held
|
|
279
|
+
var parsed: int = digits.to_int()
|
|
280
|
+
if digits.is_valid_int() and str(parsed) == digits:
|
|
281
|
+
return parsed
|
|
282
|
+
return null
|
|
283
|
+
if held is Dictionary:
|
|
284
|
+
var tagged: Dictionary = held
|
|
285
|
+
if str(tagged.get("_type", "")) == "int" and tagged.has("value"):
|
|
286
|
+
return _exact_int(tagged["value"])
|
|
287
|
+
return null
|
|
288
|
+
|
|
289
|
+
|
|
249
290
|
## The components a path reads off [param value], by the names GDScript gives them, or none for a
|
|
250
291
|
## value that has none: a vector's axes, a colour's channels, a rectangle's corners and size, a
|
|
251
292
|
## transform's basis and origin. A value of one of these is a copy wherever it is read, so a write
|
|
@@ -393,6 +434,11 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
393
434
|
if not simple.has(type) or not simple.has(typeof(rebuilt)):
|
|
394
435
|
return rebuilt
|
|
395
436
|
|
|
437
|
+
# Digits are read as digits, since read as JSON they become a double and one past 2^53 a
|
|
438
|
+
# neighbour of the number written.
|
|
439
|
+
if rebuilt is String and type == TYPE_INT and _exact_int(rebuilt) != null:
|
|
440
|
+
return _exact_int(rebuilt)
|
|
441
|
+
|
|
396
442
|
if rebuilt is String and type != TYPE_STRING:
|
|
397
443
|
var text: String = rebuilt
|
|
398
444
|
var parsed: Variant = Read.json_or_null(text)
|
|
@@ -408,13 +454,33 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
408
454
|
return type_convert(rebuilt, type)
|
|
409
455
|
|
|
410
456
|
|
|
457
|
+
## Why [param given], meant for an int, may not be the number that was sent, or "" when it is. A
|
|
458
|
+
## JSON number is a double wherever it is read, so a whole number from 2^53 on arrives as the nearest
|
|
459
|
+
## double to the one written: 9007199254740993 sent as a number arrives as 9007199254740992, and a
|
|
460
|
+
## seed written that way would be stored as its neighbour with the answer saying it was written.
|
|
461
|
+
static func inexact(given: Variant, type: int) -> String:
|
|
462
|
+
if type != TYPE_INT or (typeof(given) != TYPE_FLOAT and typeof(given) != TYPE_INT):
|
|
463
|
+
return ""
|
|
464
|
+
var number: float = Read.as_float(given)
|
|
465
|
+
if not is_finite(number) or absf(number) < EXACT:
|
|
466
|
+
return ""
|
|
467
|
+
return (
|
|
468
|
+
(
|
|
469
|
+
"%s is 2^53 or more, where a JSON number holds a whole number only to the nearest double,"
|
|
470
|
+
+ ' so it may not be the number sent: send the digits as {"_type": "int", "value": "..."}'
|
|
471
|
+
+ ", the form a read answers with"
|
|
472
|
+
)
|
|
473
|
+
% String.num(number, 0)
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
411
477
|
## How a value of [param type] is written to be read as one, for a refusal to name, or empty for a
|
|
412
478
|
## type that is not built from fields. A refusal naming only the two types left a caller to find
|
|
413
479
|
## the shape in the description of another op.
|
|
414
480
|
static func written_as(type: int) -> String:
|
|
415
481
|
var tag: String = type_string(type)
|
|
416
482
|
var alternatives: Array = REQUIRED.get(tag, [])
|
|
417
|
-
if alternatives.is_empty() or tag
|
|
483
|
+
if alternatives.is_empty() or tag in ["float", "int", "Dictionary"]:
|
|
418
484
|
return ""
|
|
419
485
|
var keys: Array = alternatives[0]
|
|
420
486
|
var parts: Array[String] = []
|
|
@@ -530,10 +596,32 @@ func _serialize_node_path(value: NodePath) -> Dictionary:
|
|
|
530
596
|
return {"path": str(value), "_type": "NodePath"}
|
|
531
597
|
|
|
532
598
|
|
|
533
|
-
#
|
|
534
|
-
#
|
|
599
|
+
# By its id, which is all an RID is from outside the server holding it, and which builds it back:
|
|
600
|
+
# a viewport's RID read off one call is what RenderingServer's render timing takes in the next.
|
|
535
601
|
func _serialize_rid(value: RID) -> Dictionary:
|
|
536
|
-
return {"id": value.get_id(), "_type": "RID"}
|
|
602
|
+
return {"id": _serialize_int(value.get_id()), "_type": "RID"}
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
# A whole number from 2^53 on as its digits, because JSON numbers are doubles on the server's side
|
|
606
|
+
# and on the engine's, and from there on neighbouring whole numbers share a double. A RefCounted's
|
|
607
|
+
# instance id is one, with its top bit set, and a resource uid another; read as numbers they came
|
|
608
|
+
# back as a neighbouring id, which named nothing or something else.
|
|
609
|
+
func _serialize_int(value: int) -> Variant:
|
|
610
|
+
if value > -EXACT and value < EXACT:
|
|
611
|
+
return value
|
|
612
|
+
return {"_type": "int", "value": str(value)}
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
# A packed list of 64-bit numbers stays the JSON list it is unless something in it is too large for
|
|
616
|
+
# a double to hold.
|
|
617
|
+
func _serialize_ints(value: PackedInt64Array) -> Variant:
|
|
618
|
+
for number: int in value:
|
|
619
|
+
if number <= -EXACT or number >= EXACT:
|
|
620
|
+
var items: Array = []
|
|
621
|
+
for each: int in value:
|
|
622
|
+
items.append(_serialize_int(each))
|
|
623
|
+
return items
|
|
624
|
+
return value
|
|
537
625
|
|
|
538
626
|
|
|
539
627
|
# A bound method, said in the terms a caller could act on: what it is on, and what it is called.
|
|
@@ -672,7 +760,7 @@ func _describe_object(value: Object) -> Dictionary:
|
|
|
672
760
|
# in a costume and reads as if the object had been asked and had nothing to say.
|
|
673
761
|
if value.has_method("_to_string"):
|
|
674
762
|
described["says"] = value.to_string()
|
|
675
|
-
described["id"] = value.get_instance_id()
|
|
763
|
+
described["id"] = _serialize_int(value.get_instance_id())
|
|
676
764
|
return described
|
|
677
765
|
|
|
678
766
|
|
|
@@ -808,6 +896,16 @@ func _build_float(fields: Dictionary) -> float:
|
|
|
808
896
|
return Read.as_float(fields)
|
|
809
897
|
|
|
810
898
|
|
|
899
|
+
func _build_int(fields: Dictionary) -> int:
|
|
900
|
+
var exact: int = _exact_int(fields["value"])
|
|
901
|
+
return exact
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
func _build_rid(fields: Dictionary) -> RID:
|
|
905
|
+
var id: int = _exact_int(fields["id"])
|
|
906
|
+
return rid_from_int64(id)
|
|
907
|
+
|
|
908
|
+
|
|
811
909
|
## The entries of a dictionary whose keys are not all text. JSON carries every number as a float,
|
|
812
910
|
## and a dictionary keys 1.0 apart from 1, so a whole-number key is built as an int: the key a
|
|
813
911
|
## game uses far more often, and one JSON cannot tell from the float.
|