gdharness 1.2.36 → 1.2.37

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.
@@ -38,6 +38,20 @@ const ScriptCheck = preload("script_check.gd")
38
38
  ## step with `ANSWERED` in src/headless.ts.
39
39
  const ANSWERED: String = "[gdharness] answered"
40
40
 
41
+ ## Printed in `_init` by an operation that runs once the project's autoloads are in the tree, and
42
+ ## when it begins there. Kept in step with `WAITING` and `BEGUN` in src/headless.ts, which files
43
+ ## what the autoloads print in between as theirs rather than the operation's.
44
+ const WAITING: String = "[gdharness] waiting for the autoloads"
45
+ const BEGUN: String = "[gdharness] begun"
46
+
47
+ ## The operations that compile the project's scripts. The engine knows an autoload's name only once
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 loads the scripts
50
+ ## of the scene it opens.
51
+ const AFTER_THE_AUTOLOADS: Array[String] = [
52
+ "check_scripts", "create_script", "modify_script", "run_editor_tool"
53
+ ]
54
+
41
55
  var _log: Log
42
56
 
43
57
 
@@ -76,12 +90,31 @@ func _init() -> void:
76
90
 
77
91
  _log.info("Executing operation: " + operation)
78
92
 
93
+ var answer_path: String = args[answer_index]
94
+ if operation in AFTER_THE_AUTOLOADS:
95
+ printerr(WAITING)
96
+ var waiting: int = process_frame.connect(
97
+ _answer_after_the_autoloads.bind(operation, params, answer_path), CONNECT_ONE_SHOT
98
+ )
99
+ if waiting != OK:
100
+ _log.error("Could not wait for the project's autoloads: " + error_string(waiting))
101
+ quit(1)
102
+ return
103
+ _answer(operation, params, answer_path)
104
+
105
+
106
+ func _answer_after_the_autoloads(operation: String, params: Dictionary, answer_path: String) -> void:
107
+ printerr(BEGUN)
108
+ _answer(operation, params, answer_path)
109
+
110
+
111
+ ## Runs [param operation] and writes its answer to [param answer_path], then quits.
112
+ func _answer(operation: String, params: Dictionary, answer_path: String) -> void:
79
113
  var payload: Dictionary = _run(operation, params)
80
114
  if payload.is_empty():
81
115
  quit(1)
82
116
  return
83
117
 
84
- var answer_path: String = args[answer_index]
85
118
  var answer: FileAccess = FileAccess.open(answer_path, FileAccess.WRITE)
86
119
  if answer == null:
87
120
  _log.error(
@@ -173,6 +206,9 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
173
206
  )
174
207
  )
175
208
 
209
+ "run_editor_tool":
210
+ payload = _read(_editor_tool(params))
211
+
176
212
  # Import and export pipeline
177
213
  "get_import_status":
178
214
  payload = ImportPipeline.new(_log).get_import_status(params)
@@ -259,6 +295,20 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
259
295
  return payload
260
296
 
261
297
 
298
+ ## A scene or resource tool of the editor addon, run as editor_tools.gd runs it. Loaded for this op
299
+ ## alone, because the tools bring the editor addon's scene and resource scripts with them: preloaded
300
+ ## here, every op compiled them, and an operations directory without the addons beside it lost every
301
+ ## op rather than this one.
302
+ func _editor_tool(params: Dictionary) -> Dictionary:
303
+ var own: Script = get_script()
304
+ var path: String = own.resource_path.get_base_dir().path_join("editor_tools.gd")
305
+ var tools: Script = ResourceLoader.load(path, "Script")
306
+ if tools == null or not tools.can_instantiate():
307
+ return {"ok": false, "error": "The editor tools did not load from %s." % path}
308
+ var answer: Dictionary = tools.call("run", params)
309
+ return answer
310
+
311
+
262
312
  ## A read the editor answers too, in the shape an operation answers: its refusal as the run's error.
263
313
  func _read(read: Dictionary) -> Dictionary:
264
314
  if not read.get("ok", false):
@@ -272,6 +272,9 @@ func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
272
272
  var elements: Array = elements_of(declared)
273
273
  if _fraction(raw, expected_type, elements):
274
274
  return [false, _fraction_refusal(target, property, raw)]
275
+ var rounded: String = Serialisation.inexact(raw, expected_type)
276
+ if not rounded.is_empty():
277
+ return [false, "%s.%s holds int: %s." % [target.get_class(), property, rounded]]
275
278
  _refused = ""
276
279
  value = parse(raw, expected_type, elements)
277
280
  if not _refused.is_empty():
@@ -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 == "float" or tag == "Dictionary":
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
- # An id rather than nothing, because two properties holding the same RID are worth telling apart
534
- # even though neither can be handed back to the engine.
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.