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.
@@ -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 = ResourceSaver.save(resource, path)
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
- var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
147
- if file == null or not file.store_buffer(bytes):
148
- return lost + " Putting the file back as it was failed too."
149
- elif DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) != OK:
150
- return lost + " Removing the file it wrote failed too."
151
- return lost
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
- if (
164
- not ClassDB.class_exists(resource_type)
165
- or not ClassDB.is_parent_class(resource_type, "Resource")
166
- or not ClassDB.can_instantiate(resource_type)
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": resource.get_class()}
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 = ResourceSaver.save(tileset, tileset_path)
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 save_result: Error = ResourceSaver.save(theme, theme_path)
648
- if save_result != OK:
649
- return {"ok": false, "error": "Failed to save %s: %s" % [theme_path, error_string(save_result)]}
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 save_result: Error = ResourceSaver.save(theme, theme_path)
684
- if save_result != OK:
685
- return {"ok": false, "error": "Failed to save %s: %s" % [theme_path, error_string(save_result)]}
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 the editor knows none of what it declares."
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: return _kept(scene, saved, new_node, properties.keys())
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 {"ok": false, "error": "Cannot delete root node"}
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
- "ok": true,
575
- "nodePath": node_path,
576
- "properties": props,
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
- for open: Node in EditorInterface.get_open_scene_roots():
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, false)
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
- return {"ok": true, "connections": connections, "unsavedInEditor": SceneFile.unsaved(scene_path)}
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 == "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.