gdharness 1.2.37 → 1.2.39
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 +943 -491
- package/build/godot/addons/gdharness_editor/headless_check.gd +41 -4
- package/build/godot/addons/gdharness_editor/scene_file.gd +86 -23
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +20 -7
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +44 -6
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +7 -4
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +10 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +8 -4
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +2 -2
- package/build/godot/operations/godot_operations.gd +10 -3
- package/build/index.js +4521 -4072
- package/build/keeper.js +1 -0
- package/package.json +1 -1
|
@@ -90,10 +90,41 @@ static func default_of(object: Object, name: String) -> Variant:
|
|
|
90
90
|
if script != null:
|
|
91
91
|
for declared: Dictionary in script.get_script_property_list():
|
|
92
92
|
if str(declared.get("name", "")) == name:
|
|
93
|
-
|
|
93
|
+
var given: Variant = script.get_property_default_value(name)
|
|
94
|
+
if given != null:
|
|
95
|
+
return given
|
|
96
|
+
return _initialised(script, name, Read.as_int(declared.get("type", TYPE_NIL)))
|
|
94
97
|
return ClassDB.class_get_property_default_value(object.get_class(), name)
|
|
95
98
|
|
|
96
99
|
|
|
100
|
+
## What the declaration of [param name] in [param script] or a script it extends initialises it to,
|
|
101
|
+
## when that is a value written out, such as [code]PackedStringArray()[/code], or null. The engine
|
|
102
|
+
## reports a default only for an initializer it folded into a constant, and it folds no constructor
|
|
103
|
+
## call, so it gives none for such a property; the saver then writes it whatever it holds, and the
|
|
104
|
+
## line read as a change some script had made.
|
|
105
|
+
static func _initialised(script: Script, name: String, type: int) -> Variant:
|
|
106
|
+
var declaration: RegEx = RegEx.create_from_string(
|
|
107
|
+
"(?m)^[ \\t]*(?:@[^\\n]*?\\s)?var\\s+" + name + "\\b[^=\\n]*?=\\s*(.+?)\\s*:?[ \\t]*$"
|
|
108
|
+
)
|
|
109
|
+
var holder: Script = script
|
|
110
|
+
while holder != null:
|
|
111
|
+
var found: RegExMatch = declaration.search(holder.source_code)
|
|
112
|
+
if found != null:
|
|
113
|
+
var value: Variant = str_to_var(found.get_string(1))
|
|
114
|
+
if value == null or type == TYPE_NIL or typeof(value) == type:
|
|
115
|
+
return value
|
|
116
|
+
return type_convert(value, type)
|
|
117
|
+
holder = holder.get_base_script()
|
|
118
|
+
return null
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
## Whether [param object] holds its default for [param name], so a file that newly writes it says
|
|
122
|
+
## nothing a script did: the saver writes a property whose default the engine cannot report whatever
|
|
123
|
+
## it holds, the editor's save as well.
|
|
124
|
+
static func at_default(object: Object, name: String) -> bool:
|
|
125
|
+
return PropertyValues.same(object.get(name), default_of(object, name))
|
|
126
|
+
|
|
127
|
+
|
|
97
128
|
## The ids, in the file, of the embedded resources [param property] reaches into on [param object],
|
|
98
129
|
## such as the material of [code]material:albedo_color[/code], as a set.
|
|
99
130
|
static func reached(object: Object, property: String) -> Dictionary:
|
|
@@ -295,17 +326,23 @@ static func settle_saved(
|
|
|
295
326
|
) -> String:
|
|
296
327
|
for attempt: int in 2:
|
|
297
328
|
var after: String = FileAccess.get_file_as_string(path)
|
|
298
|
-
var
|
|
329
|
+
var by_id: Dictionary = embedded_in(values, {})
|
|
330
|
+
var embedded: Array[Array] = []
|
|
331
|
+
for found: Array in embedded_changes(before, after, reached_ids):
|
|
332
|
+
var holder: Resource = by_id.get(found[0])
|
|
333
|
+
if found[2] or holder == null or not at_default(holder, str(found[1])):
|
|
334
|
+
embedded.append(found)
|
|
299
335
|
var top: Array[Array] = []
|
|
300
336
|
if main != null:
|
|
301
|
-
|
|
337
|
+
for found: Array in main_changes(before, after, asked):
|
|
338
|
+
if found[1] or not at_default(main, str(found[0])):
|
|
339
|
+
top.append(found)
|
|
302
340
|
if embedded.is_empty() and top.is_empty():
|
|
303
341
|
return ""
|
|
304
342
|
if attempt == 1:
|
|
305
343
|
if not embedded.is_empty():
|
|
306
344
|
return unasked("The embedded resource " + str(embedded[0][0]), str(embedded[0][1]))
|
|
307
345
|
return unasked(path, str(top[0][0]))
|
|
308
|
-
var by_id: Dictionary = embedded_in(values, {})
|
|
309
346
|
for found: Array in embedded:
|
|
310
347
|
var id: String = found[0]
|
|
311
348
|
var name: String = found[1]
|
|
@@ -26,6 +26,9 @@ extends RefCounted
|
|
|
26
26
|
## anything is saved, and a write is refused when that does not hold. See [HeadlessCheck].
|
|
27
27
|
|
|
28
28
|
const HeadlessCheck = preload("headless_check.gd")
|
|
29
|
+
|
|
30
|
+
## How many times what scripts changed is put back before a write is refused for it.
|
|
31
|
+
const PUTTING_BACK_PASSES: int = 4
|
|
29
32
|
const PropertyValues = preload("property_values.gd")
|
|
30
33
|
|
|
31
34
|
## Whether a scan asked for through [method scan_later] is still to start, which a caller waiting
|
|
@@ -235,6 +238,60 @@ static func uncompiled(script: Script, script_path: String) -> String:
|
|
|
235
238
|
)
|
|
236
239
|
|
|
237
240
|
|
|
241
|
+
## The script a project declares [param type_name] in with class_name, as {"script", "path",
|
|
242
|
+
## "base"} with base the engine class its objects are made as, or {"error"} when that script cannot
|
|
243
|
+
## make one, or an empty dictionary when no script declares it. The editor's create dialogs offer
|
|
244
|
+
## these classes beside the engine's, and ClassDB knows only the engine's.
|
|
245
|
+
static func script_class(type_name: String, kind: String) -> Dictionary:
|
|
246
|
+
for entry: Dictionary in ProjectSettings.get_global_class_list():
|
|
247
|
+
if str(entry.get("class", "")) != type_name:
|
|
248
|
+
continue
|
|
249
|
+
var declaring: String = str(entry.get("path", ""))
|
|
250
|
+
if entry.get("is_abstract", false) == true:
|
|
251
|
+
return {
|
|
252
|
+
"error":
|
|
253
|
+
"%s, declared in %s, is abstract, so nothing can be made of it." % [type_name, declaring]
|
|
254
|
+
}
|
|
255
|
+
var script: Script = load(declaring) as Script
|
|
256
|
+
if script == null:
|
|
257
|
+
return {"error": "%s is declared in %s, which does not load." % [type_name, declaring]}
|
|
258
|
+
var failed: String = uncompiled(script, declaring)
|
|
259
|
+
if not failed.is_empty():
|
|
260
|
+
return {"error": failed}
|
|
261
|
+
var base: String = str(script.get_instance_base_type())
|
|
262
|
+
if not ClassDB.is_parent_class(base, kind):
|
|
263
|
+
return {
|
|
264
|
+
"error":
|
|
265
|
+
"%s, declared in %s, extends %s, which is not a %s." % [type_name, declaring, base, kind]
|
|
266
|
+
}
|
|
267
|
+
return {"script": script, "path": declaring, "base": base}
|
|
268
|
+
return {}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
## The engine class and the script a create makes for [param type_name] and [param given], the
|
|
272
|
+
## res:// path of the script the call named or "", as {"type", "script"}, or {"error"}. A class_name
|
|
273
|
+
## a project script declares is that script on its engine class; [param argument] names the
|
|
274
|
+
## argument the type came in, for the refusal when the call also named another script.
|
|
275
|
+
static func made_as(type_name: String, given: String, kind: String, argument: String) -> Dictionary:
|
|
276
|
+
if ClassDB.class_exists(type_name):
|
|
277
|
+
return {"type": type_name, "script": given}
|
|
278
|
+
var declared: Dictionary = script_class(type_name, kind)
|
|
279
|
+
if declared.has("error"):
|
|
280
|
+
return declared
|
|
281
|
+
if declared.is_empty():
|
|
282
|
+
return {"type": type_name, "script": given}
|
|
283
|
+
var declaring: String = str(declared["path"])
|
|
284
|
+
if not given.is_empty() and given != declaring:
|
|
285
|
+
return {
|
|
286
|
+
"error":
|
|
287
|
+
(
|
|
288
|
+
"%s %s is the class %s declares, and script names %s: pass one of them."
|
|
289
|
+
% [argument, type_name, declaring, given]
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
return {"type": str(declared["base"]), "script": declaring}
|
|
293
|
+
|
|
294
|
+
|
|
238
295
|
## Why no node can be called [param node_name], wherever it sits, or "".
|
|
239
296
|
static func unfit_name(node_name: String) -> String:
|
|
240
297
|
if node_name.is_empty():
|
|
@@ -459,30 +516,34 @@ static func unsaved(scene_path: String) -> bool:
|
|
|
459
516
|
## On a node the tool did not ask to change, a difference is put back to what the file holds, or, for
|
|
460
517
|
## a property the file does not set, to what the node would load with without it. On a node it did
|
|
461
518
|
## ask to change, through a script that is not a tool, every property it did not name is put back to
|
|
462
|
-
## what it held before. Packed again afterwards
|
|
463
|
-
##
|
|
519
|
+
## what it held before. Packed again afterwards and put back again until nothing differs, since
|
|
520
|
+
## putting one property back can run a setter that changes another already put back (a script
|
|
521
|
+
## setting speed in _init, whose setter tints modulate), and refused if something still does.
|
|
464
522
|
func _settled(packed: PackedScene) -> Array:
|
|
465
|
-
var
|
|
466
|
-
var
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
for
|
|
477
|
-
node
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
523
|
+
var current: PackedScene = packed
|
|
524
|
+
var left: Array[Array] = []
|
|
525
|
+
for put_back: int in PUTTING_BACK_PASSES + 1:
|
|
526
|
+
var stray: Array[Array] = _strays(current.get_state())
|
|
527
|
+
var moved: Array[Array] = _moved_by_scripts()
|
|
528
|
+
if stray.is_empty() and moved.is_empty():
|
|
529
|
+
return [current, ""]
|
|
530
|
+
if put_back == PUTTING_BACK_PASSES:
|
|
531
|
+
left = stray
|
|
532
|
+
left.append_array(moved)
|
|
533
|
+
break
|
|
534
|
+
for found: Array in stray:
|
|
535
|
+
var node: Node = found[0]
|
|
536
|
+
var name: String = found[1]
|
|
537
|
+
node.set(name, found[3] if found[2] else _unset_value(node, name))
|
|
538
|
+
for found: Array in moved:
|
|
539
|
+
var node: Node = found[0]
|
|
540
|
+
var before: Dictionary = _before[node.get_instance_id()]
|
|
541
|
+
for name: String in found[1]:
|
|
542
|
+
node.set(name, before.get(name, HeadlessCheck.default_of(node, name)))
|
|
543
|
+
current = PackedScene.new()
|
|
544
|
+
var packing: Error = current.pack(root)
|
|
545
|
+
if packing != OK:
|
|
546
|
+
return [null, "Failed to pack %s: %s" % [path, error_string(packing)]]
|
|
486
547
|
var first: Array = left[0]
|
|
487
548
|
var stray_node: Node = first[0]
|
|
488
549
|
var named: String = str(first[1])
|
|
@@ -538,6 +599,8 @@ func _strays(state: SceneState) -> Array[Array]:
|
|
|
538
599
|
var held: bool = filed.has(name)
|
|
539
600
|
if held == stored.has(name) and (not held or PropertyValues.same(stored[name], filed[name])):
|
|
540
601
|
continue
|
|
602
|
+
if not held and HeadlessCheck.at_default(node, name):
|
|
603
|
+
continue
|
|
541
604
|
stray.append([node, name, held, filed.get(name)])
|
|
542
605
|
return stray
|
|
543
606
|
|
|
@@ -194,23 +194,36 @@ func _unkept_headless(resource: Resource, path: String, names: Array, previous:
|
|
|
194
194
|
func create_resource(args: Dictionary) -> Dictionary:
|
|
195
195
|
var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
|
|
196
196
|
var resource_type: String = str(args.get("resourceType", "Resource"))
|
|
197
|
+
var asked_type: String = resource_type
|
|
197
198
|
if res_path == "res://":
|
|
198
199
|
return {"ok": false, "error": "resourcePath is required"}
|
|
199
200
|
var taken: String = _taken(res_path, "resource_edit op=create")
|
|
200
201
|
if not taken.is_empty():
|
|
201
202
|
return {"ok": false, "error": taken}
|
|
202
203
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
):
|
|
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):
|
|
208
222
|
return {"ok": false, "error": "%s is not a resource class that can be created." % resource_type}
|
|
209
223
|
var resource: Resource = ClassDB.instantiate(resource_type)
|
|
210
224
|
|
|
211
225
|
# A script that did not load was skipped, and a plain resource was saved in place of the custom
|
|
212
226
|
# one asked for.
|
|
213
|
-
var script_path: String = str(args.get("script", ""))
|
|
214
227
|
if script_path != "":
|
|
215
228
|
var script_file: String = _ensure_res_path(script_path)
|
|
216
229
|
var script: Script = load(script_file) as Script
|
|
@@ -238,7 +251,7 @@ func create_resource(args: Dictionary) -> Dictionary:
|
|
|
238
251
|
var failed: String = _save_checked(resource, res_path, properties.keys(), null)
|
|
239
252
|
if not failed.is_empty():
|
|
240
253
|
return {"ok": false, "error": failed}
|
|
241
|
-
return {"ok": true, "resourcePath": res_path, "resourceType":
|
|
254
|
+
return {"ok": true, "resourcePath": res_path, "resourceType": asked_type}
|
|
242
255
|
|
|
243
256
|
|
|
244
257
|
func modify_resource(args: Dictionary) -> Dictionary:
|
|
@@ -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"):
|
|
@@ -237,6 +248,9 @@ func create_scene(args: Dictionary) -> Dictionary:
|
|
|
237
248
|
"%s extends %s, so it cannot be on a %s root." % [full_script_path, base, root_node_type]
|
|
238
249
|
}
|
|
239
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
|
|
240
254
|
|
|
241
255
|
var folder: String = SceneFile.make_folder_for(scene_path)
|
|
242
256
|
if not folder.is_empty():
|
|
@@ -246,9 +260,7 @@ func create_scene(args: Dictionary) -> Dictionary:
|
|
|
246
260
|
if not written.is_empty():
|
|
247
261
|
return written
|
|
248
262
|
|
|
249
|
-
return {
|
|
250
|
-
"ok": true, "scenePath": scene_path, "rootNodeType": root_node_type, "rootNodeName": root_node_name
|
|
251
|
-
}
|
|
263
|
+
return {"ok": true, "scenePath": scene_path, "rootNodeType": asked_type, "rootNodeName": root_node_name}
|
|
252
264
|
|
|
253
265
|
|
|
254
266
|
func list_scene_nodes(args: Dictionary) -> Dictionary:
|
|
@@ -276,6 +288,16 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
276
288
|
|
|
277
289
|
if node_name.is_empty():
|
|
278
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"])
|
|
279
301
|
var making: Array = Placing.what_to_add(node_type, instance_path, scene_path)
|
|
280
302
|
var packed: PackedScene = making[0]
|
|
281
303
|
if not str(making[1]).is_empty():
|
|
@@ -304,6 +326,9 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
304
326
|
|
|
305
327
|
# In the tree before its properties, so a property holding a node can find it by path.
|
|
306
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)
|
|
307
332
|
new_node.name = node_name
|
|
308
333
|
parent.add_child(new_node)
|
|
309
334
|
Placing.place(new_node, place)
|
|
@@ -321,12 +346,15 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
321
346
|
var answer: Dictionary = {
|
|
322
347
|
"ok": true,
|
|
323
348
|
"nodeName": node_name,
|
|
324
|
-
"nodeType": new_node.get_class(),
|
|
349
|
+
"nodeType": asked_type if class_script != null else new_node.get_class(),
|
|
325
350
|
"nodePath": new_path,
|
|
326
351
|
"index": new_node.get_index(),
|
|
327
352
|
}
|
|
328
353
|
var written: Dictionary = scene.write(
|
|
329
|
-
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
|
+
)
|
|
330
358
|
)
|
|
331
359
|
if not written.is_empty():
|
|
332
360
|
return written
|
|
@@ -342,7 +370,14 @@ func delete_node(args: Dictionary) -> Dictionary:
|
|
|
342
370
|
var node_path: String = str(args.get("nodePath", ""))
|
|
343
371
|
|
|
344
372
|
if node_path.is_empty() or node_path == ".":
|
|
345
|
-
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
|
+
}
|
|
346
381
|
|
|
347
382
|
var opened: Array = _open(scene_path, true)
|
|
348
383
|
var scene: SceneFile = opened[0]
|
|
@@ -480,6 +515,9 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
480
515
|
return scene.refuse(unplaceable)
|
|
481
516
|
|
|
482
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
|
|
483
521
|
node.get_parent().remove_child(node)
|
|
484
522
|
new_parent.add_child(node)
|
|
485
523
|
Placing.place(node, place)
|
|
@@ -352,11 +352,14 @@ func _process(_delta: float) -> void:
|
|
|
352
352
|
_queries.note_frame(Time.get_ticks_usec())
|
|
353
353
|
_calibrate_the_profiler()
|
|
354
354
|
|
|
355
|
-
|
|
355
|
+
# Every connection waiting rather than one a frame: the engine queues eight a game has not
|
|
356
|
+
# taken and refuses the rest, so calls sent together would be refused as if the game were not up.
|
|
357
|
+
while _server.is_connection_available():
|
|
356
358
|
var client: StreamPeerTCP = _server.take_connection()
|
|
357
|
-
if client:
|
|
358
|
-
|
|
359
|
-
|
|
359
|
+
if client == null:
|
|
360
|
+
break
|
|
361
|
+
_clients.append(client)
|
|
362
|
+
_send_welcome(client)
|
|
360
363
|
|
|
361
364
|
var gone: Array[StreamPeerTCP] = []
|
|
362
365
|
for client: StreamPeerTCP in _clients:
|
|
@@ -343,6 +343,16 @@ func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false
|
|
|
343
343
|
var screenshot_path: String = requested_path
|
|
344
344
|
if screenshot_path.begins_with("user://") or screenshot_path.begins_with("res://"):
|
|
345
345
|
screenshot_path = ProjectSettings.globalize_path(screenshot_path)
|
|
346
|
+
# The server makes the folder for each capture and takes it away when it stops waiting for one,
|
|
347
|
+
# and a server replaced since the capture was asked for took it with it. Saving into a folder that
|
|
348
|
+
# is gone raises an engine error, which counts against a game that did nothing wrong.
|
|
349
|
+
if not DirAccess.dir_exists_absolute(screenshot_path.get_base_dir()):
|
|
350
|
+
return _refused(
|
|
351
|
+
(
|
|
352
|
+
"The folder to save the capture in, %s, is gone: the server that asked for it stopped waiting"
|
|
353
|
+
% screenshot_path.get_base_dir()
|
|
354
|
+
)
|
|
355
|
+
)
|
|
346
356
|
var save_error: Error = image.save_png(screenshot_path)
|
|
347
357
|
if save_error != OK:
|
|
348
358
|
return {"type": "error", "message": "Failed to save screenshot as PNG: " + str(save_error)}
|
|
@@ -164,8 +164,8 @@ func _argument_for(
|
|
|
164
164
|
"type": "error",
|
|
165
165
|
"message":
|
|
166
166
|
(
|
|
167
|
-
"%s.%s takes %s as argument %d and was given %s, which
|
|
168
|
-
% [called, named, type_string(wants), index + 1, JSON.stringify(sent)]
|
|
167
|
+
"%s.%s takes %s as argument %d and was given %s, which %s cannot hold without changing it"
|
|
168
|
+
% [called, named, type_string(wants), index + 1, JSON.stringify(sent), Values.cut_by(wants)]
|
|
169
169
|
)
|
|
170
170
|
}
|
|
171
171
|
var rounded: String = Values.inexact(sent, wants)
|
|
@@ -259,8 +259,8 @@ func _value_for(
|
|
|
259
259
|
"type": "error",
|
|
260
260
|
"message":
|
|
261
261
|
(
|
|
262
|
-
"%s.%s holds %s and the value given is %s, which
|
|
263
|
-
% [called, named, type_string(wanted), JSON.stringify(value)]
|
|
262
|
+
"%s.%s holds %s and the value given is %s, which %s cannot hold without changing it"
|
|
263
|
+
% [called, named, type_string(wanted), JSON.stringify(value), Values.cut_by(wanted)]
|
|
264
264
|
)
|
|
265
265
|
}
|
|
266
266
|
if declared != TYPE_NIL or (not cut and Values.acceptable(as_sent, wanted)):
|
|
@@ -563,6 +563,10 @@ static func _walk_from(object: Object, path: String) -> Dictionary:
|
|
|
563
563
|
## null, and the method list says how many a method takes: its parameters less those with defaults,
|
|
564
564
|
## up to all of them, or any number past that for one taking varargs.
|
|
565
565
|
static func _argument_count_refused(holder: Object, method: String, given: int) -> String:
|
|
566
|
+
# A node's method list runs to hundreds of entries, and walking it costs a couple of milliseconds
|
|
567
|
+
# of the game's frame; a call giving exactly the arguments the method declares needs no walk.
|
|
568
|
+
if given == holder.get_method_argument_count(method):
|
|
569
|
+
return ""
|
|
566
570
|
for entry: Dictionary in holder.get_method_list():
|
|
567
571
|
if str(entry.get("name", "")) != method:
|
|
568
572
|
continue
|
|
@@ -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
|
+
## The slot [method cuts] speaks of, with its article, for a refusal to name: "an int" or "a bool".
|
|
160
|
+
static func cut_by(type: int) -> String:
|
|
161
|
+
return "an int" if type == TYPE_INT else "a bool"
|
|
162
|
+
|
|
163
|
+
|
|
159
164
|
## Why [param given] may not be the whole number sent, or "": see [method Serialisation.inexact].
|
|
160
165
|
static func inexact(given: Variant, type: int) -> String:
|
|
161
166
|
return Serialisation.inexact(given, type)
|
|
@@ -411,10 +411,10 @@ static func _cut_to_fit(given: Variant, type: int, node_path: String, property:
|
|
|
411
411
|
return ""
|
|
412
412
|
return (
|
|
413
413
|
(
|
|
414
|
-
"%s.%s holds %s and the value to wait for is %s, which
|
|
414
|
+
"%s.%s holds %s and the value to wait for is %s, which %s cannot hold without changing it:"
|
|
415
415
|
+ " waiting on it would answer about a state nobody asked for."
|
|
416
416
|
)
|
|
417
|
-
% [node_path, property, type_string(type), str(given)]
|
|
417
|
+
% [node_path, property, type_string(type), str(given), Values.cut_by(type)]
|
|
418
418
|
)
|
|
419
419
|
|
|
420
420
|
|
|
@@ -46,10 +46,17 @@ const BEGUN: String = "[gdharness] begun"
|
|
|
46
46
|
|
|
47
47
|
## The operations that compile the project's scripts. The engine knows an autoload's name only once
|
|
48
48
|
## the autoload is in the tree, which is after `_init`, so a script naming one compiled here failed
|
|
49
|
-
## with "Identifier not found" while the game and the editor ran it. A scene tool
|
|
50
|
-
## of the
|
|
49
|
+
## with "Identifier not found" while the game and the editor ran it. A scene tool, and a read of a
|
|
50
|
+
## scene or a resource, loads the scripts of the file it opens.
|
|
51
51
|
const AFTER_THE_AUTOLOADS: Array[String] = [
|
|
52
|
-
"check_scripts",
|
|
52
|
+
"check_scripts",
|
|
53
|
+
"create_script",
|
|
54
|
+
"modify_script",
|
|
55
|
+
"read_animation_states",
|
|
56
|
+
"read_animations",
|
|
57
|
+
"read_resource",
|
|
58
|
+
"read_scene_tree",
|
|
59
|
+
"run_editor_tool",
|
|
53
60
|
]
|
|
54
61
|
|
|
55
62
|
var _log: Log
|