gdharness 1.0.22 → 1.0.24
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.
|
@@ -6,6 +6,51 @@ extends RefCounted
|
|
|
6
6
|
|
|
7
7
|
const Values = preload("runtime_values.gd")
|
|
8
8
|
|
|
9
|
+
## The calls a path makes on a list or a map: those that take no arguments and change nothing.
|
|
10
|
+
##
|
|
11
|
+
## Named rather than asked of the engine, because asking about a method a list does not have logs an
|
|
12
|
+
## error into the game's output, and a typo in a path is not the game's error. The ones that change
|
|
13
|
+
## what they are called on, such as clear(), sort() and pop_back(), are left out: a path is how a
|
|
14
|
+
## read and a wait reach what they read, and a wait asks many times.
|
|
15
|
+
const LIST_CALLS: Array[String] = [
|
|
16
|
+
"size",
|
|
17
|
+
"is_empty",
|
|
18
|
+
"front",
|
|
19
|
+
"back",
|
|
20
|
+
"max",
|
|
21
|
+
"min",
|
|
22
|
+
"pick_random",
|
|
23
|
+
"hash",
|
|
24
|
+
"duplicate",
|
|
25
|
+
"duplicate_deep",
|
|
26
|
+
"is_read_only",
|
|
27
|
+
"is_typed",
|
|
28
|
+
"get_typed_builtin",
|
|
29
|
+
"get_typed_class_name",
|
|
30
|
+
"get_typed_script",
|
|
31
|
+
]
|
|
32
|
+
const MAP_CALLS: Array[String] = [
|
|
33
|
+
"size",
|
|
34
|
+
"is_empty",
|
|
35
|
+
"keys",
|
|
36
|
+
"values",
|
|
37
|
+
"hash",
|
|
38
|
+
"duplicate",
|
|
39
|
+
"duplicate_deep",
|
|
40
|
+
"is_read_only",
|
|
41
|
+
"is_typed",
|
|
42
|
+
"is_typed_key",
|
|
43
|
+
"is_typed_value",
|
|
44
|
+
"get_typed_key_builtin",
|
|
45
|
+
"get_typed_key_class_name",
|
|
46
|
+
"get_typed_key_script",
|
|
47
|
+
"get_typed_value_builtin",
|
|
48
|
+
"get_typed_value_class_name",
|
|
49
|
+
"get_typed_value_script",
|
|
50
|
+
]
|
|
51
|
+
## The calls an empty list has nothing to answer, which the engine reports as an error when asked.
|
|
52
|
+
const NEEDS_AN_ELEMENT: Array[String] = ["front", "back", "pick_random"]
|
|
53
|
+
|
|
9
54
|
|
|
10
55
|
## What [param reaching] names something on, which is the node itself until the name has a colon
|
|
11
56
|
## in it, and the last name along that path.
|
|
@@ -23,7 +68,9 @@ const Values = preload("runtime_values.gd")
|
|
|
23
68
|
## and walks into what it returned. Some of what hangs off a node is only reachable by asking:
|
|
24
69
|
## which control holds the focus is a question for the viewport a control is in, and the viewport
|
|
25
70
|
## is a method's answer rather than a property. Spelled with the brackets so a reader sees a call
|
|
26
|
-
## where one happens, and so a method can never shadow a property of the same name.
|
|
71
|
+
## where one happens, and so a method can never shadow a property of the same name. A list or a map
|
|
72
|
+
## answers the calls that read it, "board:size()" or "inbox:keys()", listed in [constant LIST_CALLS]
|
|
73
|
+
## and [constant MAP_CALLS].
|
|
27
74
|
##
|
|
28
75
|
## Answers with a `message` instead when a step along the way is not there or holds something that
|
|
29
76
|
## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
|
|
@@ -117,9 +164,14 @@ static func nothing_under(holder: Variant, named: String, called: String) -> Str
|
|
|
117
164
|
static func can_read(holder: Variant, named: String) -> bool:
|
|
118
165
|
if holder is Array:
|
|
119
166
|
var items: Array = holder
|
|
167
|
+
var list_call: String = method_of(named)
|
|
168
|
+
if not list_call.is_empty():
|
|
169
|
+
return list_call in LIST_CALLS and not (items.is_empty() and list_call in NEEDS_AN_ELEMENT)
|
|
120
170
|
return _index_in(named, items.size()) != -1
|
|
121
171
|
if holder is Dictionary:
|
|
122
172
|
var map: Dictionary = holder
|
|
173
|
+
if not method_of(named).is_empty():
|
|
174
|
+
return method_of(named) in MAP_CALLS
|
|
123
175
|
return map.has(named) or map.has(StringName(named))
|
|
124
176
|
if holder is Object:
|
|
125
177
|
var object: Object = holder
|
|
@@ -173,6 +225,8 @@ static func write(holder: Variant, named: String, value: Variant) -> void:
|
|
|
173
225
|
## What [param holder] holds under [param named], or answers to it when [param named] is a call.
|
|
174
226
|
## Only ever called once [method can_read] agrees.
|
|
175
227
|
static func read_under(holder: Variant, named: String) -> Variant:
|
|
228
|
+
if (holder is Array or holder is Dictionary) and not method_of(named).is_empty():
|
|
229
|
+
return Callable.create(holder, method_of(named)).call()
|
|
176
230
|
if holder is Array:
|
|
177
231
|
var items: Array = holder
|
|
178
232
|
return items[_index_in(named, items.size())]
|
|
@@ -207,10 +261,17 @@ static func _index_in(named: String, size: int) -> int:
|
|
|
207
261
|
## so a list says how long it is and a map says what it is keyed by. The step is named rather than
|
|
208
262
|
## the whole path, because the step is the typo.
|
|
209
263
|
static func nothing_there(holder: Variant, named: String, called_as: String) -> String:
|
|
264
|
+
var container_call: String = method_of(named)
|
|
210
265
|
if holder is Array:
|
|
211
266
|
var items: Array = holder
|
|
267
|
+
if container_call in NEEDS_AN_ELEMENT and items.is_empty():
|
|
268
|
+
return "%s is an empty list, so it has no %s" % [called_as, named]
|
|
269
|
+
if not container_call.is_empty():
|
|
270
|
+
return container_calls_refused(called_as, "a list", container_call)
|
|
212
271
|
return "%s is a list of %d, so there is no %s in it" % [called_as, items.size(), named]
|
|
213
272
|
if holder is Dictionary:
|
|
273
|
+
if not container_call.is_empty():
|
|
274
|
+
return container_calls_refused(called_as, "a map", container_call)
|
|
214
275
|
var map: Dictionary = holder
|
|
215
276
|
var keys: Array = map.keys()
|
|
216
277
|
# Eight of them, because a map keyed by something unexpected is told by the first few and a
|
|
@@ -242,6 +303,19 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
242
303
|
return "%s has no property %s" % [called_as, named]
|
|
243
304
|
|
|
244
305
|
|
|
306
|
+
## Why [param method] is not a call a path makes on [param sort] ("a list" or "a map"), naming
|
|
307
|
+
## the calls it does make.
|
|
308
|
+
static func container_calls_refused(called_as: String, sort: String, method: String) -> String:
|
|
309
|
+
var calls: Array[String] = LIST_CALLS if sort == "a list" else MAP_CALLS
|
|
310
|
+
var spelled: Array[String] = []
|
|
311
|
+
for each: String in calls:
|
|
312
|
+
spelled.append(each + "()")
|
|
313
|
+
return (
|
|
314
|
+
"%s is %s, and %s() is not one of the calls a path makes on one, which read it and take no arguments: %s"
|
|
315
|
+
% [called_as, sort, method, ", ".join(spelled)]
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
245
319
|
## Whether [param holder] declares [param named]. Asked of the list rather than read, because a
|
|
246
320
|
## property read back as null answers it wrongly.
|
|
247
321
|
static func _has_property(holder: Object, named: String) -> bool:
|
|
@@ -629,14 +629,49 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
629
629
|
% [reached["called"], named, type_string(wanted), type_string(typeof(given))]
|
|
630
630
|
)
|
|
631
631
|
}
|
|
632
|
+
var typed: Dictionary = _values.typed_like(given, old_value, _element_object.bind(node, node_path))
|
|
633
|
+
if typed.has("message"):
|
|
634
|
+
return {
|
|
635
|
+
"type": "error",
|
|
636
|
+
"message":
|
|
637
|
+
"%s.%s is a typed %s: %s." % [reached["called"], named, _sort_of(old_value), typed["message"]]
|
|
638
|
+
}
|
|
639
|
+
given = typed["value"]
|
|
632
640
|
Paths.write(holder, named, given)
|
|
641
|
+
var now: Variant = Paths.read_under(holder, named)
|
|
642
|
+
# Read back rather than trusted. The engine drops a write it will not take without a word, and
|
|
643
|
+
# the answer then showed the old value as the new one, shaped as a success: a typed container
|
|
644
|
+
# refusing a plain list did exactly that. A value the property changed on the way in, a setter
|
|
645
|
+
# clamping it, still changed it, so only a write that left the property as it was is refused.
|
|
646
|
+
if (
|
|
647
|
+
Values.comparable(given, old_value)
|
|
648
|
+
and given != old_value
|
|
649
|
+
and Values.comparable(now, old_value)
|
|
650
|
+
and now == old_value
|
|
651
|
+
):
|
|
652
|
+
return {
|
|
653
|
+
"type": "error",
|
|
654
|
+
"message":
|
|
655
|
+
(
|
|
656
|
+
(
|
|
657
|
+
"%s.%s was given %s and still holds %s: the engine kept what it had,"
|
|
658
|
+
+ " which a property it will not write does."
|
|
659
|
+
)
|
|
660
|
+
% [
|
|
661
|
+
reached["called"],
|
|
662
|
+
named,
|
|
663
|
+
JSON.stringify(_values.serialize(given)),
|
|
664
|
+
JSON.stringify(_values.serialize(now))
|
|
665
|
+
]
|
|
666
|
+
)
|
|
667
|
+
}
|
|
633
668
|
|
|
634
669
|
return {
|
|
635
670
|
"type": "property_set",
|
|
636
671
|
"path": node_path,
|
|
637
672
|
"property": property,
|
|
638
673
|
"old_value": _values.serialize(old_value),
|
|
639
|
-
"new_value": _values.serialize(
|
|
674
|
+
"new_value": _values.serialize(now)
|
|
640
675
|
}
|
|
641
676
|
|
|
642
677
|
|
|
@@ -660,17 +695,33 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
660
695
|
if reached.has("message"):
|
|
661
696
|
return reached
|
|
662
697
|
|
|
663
|
-
# A list or a map
|
|
664
|
-
#
|
|
665
|
-
#
|
|
698
|
+
# A list or a map answers the calls a path makes on one, and nothing else: said in those terms
|
|
699
|
+
# rather than as "has no method", which reads as a misspelling of a method that was never going
|
|
700
|
+
# to be there, and with the element spelled out, since a method on what it holds is the usual aim.
|
|
666
701
|
if not reached["holder"] is Object:
|
|
667
702
|
var sort: String = "a list" if reached["holder"] is Array else "a map"
|
|
703
|
+
var spelled: String = str(reached["name"])
|
|
704
|
+
if Paths.method_of(spelled).is_empty():
|
|
705
|
+
spelled += "()"
|
|
706
|
+
if args.is_empty() and Paths.can_read(reached["holder"], spelled):
|
|
707
|
+
var answered: Variant = Paths.read_under(reached["holder"], spelled)
|
|
708
|
+
return {
|
|
709
|
+
"type": "method_result",
|
|
710
|
+
"path": node_path,
|
|
711
|
+
"method": method,
|
|
712
|
+
"result": _values.serialize(answered),
|
|
713
|
+
}
|
|
714
|
+
var why: String = (
|
|
715
|
+
"%s:%s takes no arguments" % [reached["called"], spelled]
|
|
716
|
+
if Paths.can_read(reached["holder"], spelled)
|
|
717
|
+
else Paths.nothing_there(reached["holder"], spelled, str(reached["called"]))
|
|
718
|
+
)
|
|
668
719
|
return {
|
|
669
720
|
"type": "error",
|
|
670
721
|
"message":
|
|
671
722
|
(
|
|
672
|
-
"%s
|
|
673
|
-
% [
|
|
723
|
+
"%s. A method of what %s holds is called on the element, as in %s:0:%s"
|
|
724
|
+
% [why, sort, reached["called"], reached["name"]]
|
|
674
725
|
)
|
|
675
726
|
}
|
|
676
727
|
var holder: Object = reached["holder"]
|
|
@@ -711,6 +762,23 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
711
762
|
% [reached["called"], named, type_string(wants), index + 1, type_string(typeof(given))]
|
|
712
763
|
)
|
|
713
764
|
}
|
|
765
|
+
# A typed list or map is built as one, since a plain one handed to such a parameter raises
|
|
766
|
+
# inside the game, which is what every check here exists to stop.
|
|
767
|
+
var container: Variant = _values.parameter_container(holder, named, index)
|
|
768
|
+
if container != null:
|
|
769
|
+
var typed: Dictionary = _values.typed_like(
|
|
770
|
+
given, container, _element_object.bind(node, node_path)
|
|
771
|
+
)
|
|
772
|
+
if typed.has("message"):
|
|
773
|
+
return {
|
|
774
|
+
"type": "error",
|
|
775
|
+
"message":
|
|
776
|
+
(
|
|
777
|
+
"%s.%s takes a typed %s as argument %d: %s."
|
|
778
|
+
% [reached["called"], named, _sort_of(container), index + 1, typed["message"]]
|
|
779
|
+
)
|
|
780
|
+
}
|
|
781
|
+
given = typed["value"]
|
|
714
782
|
deserialized_args.append(given)
|
|
715
783
|
|
|
716
784
|
var result: Variant = holder.callv(named, deserialized_args)
|
|
@@ -718,6 +786,17 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
718
786
|
return {"type": "method_result", "path": node_path, "method": method, "result": _values.serialize(result)}
|
|
719
787
|
|
|
720
788
|
|
|
789
|
+
## [method _object_named] with the path first, which is the order [method Values.typed_like] calls
|
|
790
|
+
## an element resolver in.
|
|
791
|
+
func _element_object(given: Variant, declared: String, node: Node, node_path: String) -> Dictionary:
|
|
792
|
+
return _object_named(node, node_path, given, declared)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
## "list" or "map", for a refusal about a typed container.
|
|
796
|
+
static func _sort_of(container: Variant) -> String:
|
|
797
|
+
return "list" if container is Array else "map"
|
|
798
|
+
|
|
799
|
+
|
|
721
800
|
## The object [param given] names for a slot or a parameter declared as [param declared], as
|
|
722
801
|
## `{"object": ...}`, or `{"message": ...}` saying why it names none that fits.
|
|
723
802
|
##
|
|
@@ -163,3 +163,149 @@ static func class_of(object: Object) -> String:
|
|
|
163
163
|
## A value from the wire fitted to the type a property or parameter declares.
|
|
164
164
|
func fitted(value: Variant, type: int) -> Variant:
|
|
165
165
|
return _shared.fitted(value, type)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
## An empty list or map typed the way argument [param index] of [param method] declares, or null
|
|
169
|
+
## when that argument is not a typed container. What [method typed_like] builds against.
|
|
170
|
+
func parameter_container(object: Object, method: String, index: int) -> Variant:
|
|
171
|
+
for entry: Dictionary in object.get_method_list():
|
|
172
|
+
if entry.get("name", "") != method:
|
|
173
|
+
continue
|
|
174
|
+
var params: Array = entry.get("args", [])
|
|
175
|
+
if index < 0 or index >= params.size():
|
|
176
|
+
return null
|
|
177
|
+
var parameter: Dictionary = params[index]
|
|
178
|
+
var hint: int = Read.as_int(parameter.get("hint", 0), 0)
|
|
179
|
+
var named: String = str(parameter.get("hint_string", ""))
|
|
180
|
+
if hint == PROPERTY_HINT_ARRAY_TYPE:
|
|
181
|
+
var element: Dictionary = _element_type(named)
|
|
182
|
+
if element.is_empty():
|
|
183
|
+
return null
|
|
184
|
+
var builtin: int = element["builtin"]
|
|
185
|
+
var engine_class: StringName = element["class"]
|
|
186
|
+
return Array([], builtin, engine_class, element["script"])
|
|
187
|
+
if hint == PROPERTY_HINT_DICTIONARY_TYPE and named.contains(";"):
|
|
188
|
+
var key: Dictionary = _element_type(named.get_slice(";", 0))
|
|
189
|
+
var entry_type: Dictionary = _element_type(named.get_slice(";", 1))
|
|
190
|
+
if key.is_empty() or entry_type.is_empty():
|
|
191
|
+
return null
|
|
192
|
+
var key_builtin: int = key["builtin"]
|
|
193
|
+
var key_class: StringName = key["class"]
|
|
194
|
+
var value_builtin: int = entry_type["builtin"]
|
|
195
|
+
var value_class: StringName = entry_type["class"]
|
|
196
|
+
return Dictionary(
|
|
197
|
+
{}, key_builtin, key_class, key["script"], value_builtin, value_class, entry_type["script"]
|
|
198
|
+
)
|
|
199
|
+
return null
|
|
200
|
+
return null
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
## The type a container hint names, as `{"builtin", "class", "script"}`, or {} for a name this
|
|
204
|
+
## cannot resolve. A built-in type by its name, an engine class, or a script class by its global
|
|
205
|
+
## name, which a typed container holds as the class it extends and the script itself.
|
|
206
|
+
static func _element_type(named: String) -> Dictionary:
|
|
207
|
+
for type: int in TYPE_MAX:
|
|
208
|
+
if type != TYPE_OBJECT and type_string(type) == named:
|
|
209
|
+
return {"builtin": type, "class": &"", "script": null}
|
|
210
|
+
if ClassDB.class_exists(named):
|
|
211
|
+
return {"builtin": TYPE_OBJECT, "class": StringName(named), "script": null}
|
|
212
|
+
for global: Dictionary in ProjectSettings.get_global_class_list():
|
|
213
|
+
if str(global.get("class", "")) == named:
|
|
214
|
+
var script: Script = load(str(global.get("path", "")))
|
|
215
|
+
if script == null:
|
|
216
|
+
return {}
|
|
217
|
+
return {"builtin": TYPE_OBJECT, "class": script.get_instance_base_type(), "script": script}
|
|
218
|
+
return {}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
## [param given] rebuilt as the typed list or map [param like] is, as `{"value": ...}`, or
|
|
222
|
+
## `{"message": ...}` naming the element that cannot become what the container holds.
|
|
223
|
+
##
|
|
224
|
+
## A typed container refuses a plain one, and the engine says nothing about it: an Array[int]
|
|
225
|
+
## property written with the list JSON carries kept what it held, and the answer showed the write
|
|
226
|
+
## as done. Each element is fitted the way a single value is, and an object element is named by
|
|
227
|
+
## its path through [param resolve], which is called with the path and the class declared and
|
|
228
|
+
## answers the way an object argument does. Anything that is not a typed container comes back as
|
|
229
|
+
## it was given.
|
|
230
|
+
func typed_like(given: Variant, like: Variant, resolve: Callable) -> Dictionary:
|
|
231
|
+
if like is Array and given is Array:
|
|
232
|
+
var template: Array = like
|
|
233
|
+
if not template.is_typed():
|
|
234
|
+
return {"value": given}
|
|
235
|
+
var items: Array = given
|
|
236
|
+
var built: Array = []
|
|
237
|
+
for index: int in items.size():
|
|
238
|
+
var element: Dictionary = _element(
|
|
239
|
+
items[index],
|
|
240
|
+
template.get_typed_builtin(),
|
|
241
|
+
_typed_class(template.get_typed_script(), template.get_typed_class_name()),
|
|
242
|
+
resolve
|
|
243
|
+
)
|
|
244
|
+
if element.has("message"):
|
|
245
|
+
return {"message": "element %d %s" % [index, element["message"]]}
|
|
246
|
+
built.append(element["value"])
|
|
247
|
+
return {
|
|
248
|
+
"value":
|
|
249
|
+
Array(
|
|
250
|
+
built,
|
|
251
|
+
template.get_typed_builtin(),
|
|
252
|
+
template.get_typed_class_name(),
|
|
253
|
+
template.get_typed_script()
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
if like is Dictionary and given is Dictionary:
|
|
257
|
+
var map_like: Dictionary = like
|
|
258
|
+
if not map_like.is_typed():
|
|
259
|
+
return {"value": given}
|
|
260
|
+
var entries: Dictionary = given
|
|
261
|
+
var rebuilt: Dictionary = {}
|
|
262
|
+
for key: Variant in entries:
|
|
263
|
+
var key_fitted: Dictionary = _element(
|
|
264
|
+
key,
|
|
265
|
+
map_like.get_typed_key_builtin(),
|
|
266
|
+
_typed_class(map_like.get_typed_key_script(), map_like.get_typed_key_class_name()),
|
|
267
|
+
resolve
|
|
268
|
+
)
|
|
269
|
+
if key_fitted.has("message"):
|
|
270
|
+
return {"message": "key %s %s" % [str(key), key_fitted["message"]]}
|
|
271
|
+
var value_fitted: Dictionary = _element(
|
|
272
|
+
entries[key],
|
|
273
|
+
map_like.get_typed_value_builtin(),
|
|
274
|
+
_typed_class(map_like.get_typed_value_script(), map_like.get_typed_value_class_name()),
|
|
275
|
+
resolve
|
|
276
|
+
)
|
|
277
|
+
if value_fitted.has("message"):
|
|
278
|
+
return {"message": "the value under %s %s" % [str(key), value_fitted["message"]]}
|
|
279
|
+
rebuilt[key_fitted["value"]] = value_fitted["value"]
|
|
280
|
+
return {
|
|
281
|
+
"value":
|
|
282
|
+
Dictionary(
|
|
283
|
+
rebuilt,
|
|
284
|
+
map_like.get_typed_key_builtin(),
|
|
285
|
+
map_like.get_typed_key_class_name(),
|
|
286
|
+
map_like.get_typed_key_script(),
|
|
287
|
+
map_like.get_typed_value_builtin(),
|
|
288
|
+
map_like.get_typed_value_class_name(),
|
|
289
|
+
map_like.get_typed_value_script()
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
return {"value": given}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
## One element fitted to a container's element type, as `{"value"}` or `{"message"}` saying what it
|
|
296
|
+
## was and what goes there. The message is the part after "element N".
|
|
297
|
+
func _element(item: Variant, builtin: int, declared: String, resolve: Callable) -> Dictionary:
|
|
298
|
+
if builtin == TYPE_NIL:
|
|
299
|
+
return {"value": item}
|
|
300
|
+
if builtin == TYPE_OBJECT and item is String:
|
|
301
|
+
var named: Dictionary = resolve.call(item, declared)
|
|
302
|
+
if named.has("message"):
|
|
303
|
+
return {"message": str(named["message"])}
|
|
304
|
+
return {"value": named["object"]}
|
|
305
|
+
var fitted_item: Variant = fitted(item, builtin)
|
|
306
|
+
if not acceptable(fitted_item, builtin):
|
|
307
|
+
# The value as it was sent rather than its engine type: JSON numbers arrive as floats, so a 7
|
|
308
|
+
# the caller wrote was named "float".
|
|
309
|
+
var wanted: String = declared if builtin == TYPE_OBJECT else type_string(builtin)
|
|
310
|
+
return {"message": "is %s, which cannot become %s" % [JSON.stringify(serialize(item)), wanted]}
|
|
311
|
+
return {"value": fitted_item}
|