gdharness 1.2.28 → 1.2.29

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.
@@ -93,23 +93,37 @@ const READS_ONLY: String = ", and a path, which a wait reads every frame, only c
93
93
  ## "get_theme_stylebox(\"panel\"):content_margin_top", since what a control's theme gives it is only
94
94
  ## reachable by naming what is asked for; see [method arguments_of].
95
95
  ##
96
+ ## A step into a vector, a colour, a rectangle or a transform reads one of its components,
97
+ ## "size:x" or "global_transform:origin:y", by the names GDScript gives them; see
98
+ ## [method Values.components_of].
99
+ ##
96
100
  ## Answers with a `message` instead when a step along the way is not there or holds something that
97
101
  ## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
98
- ## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle.
99
- static func walk_to(node: Node, node_path: String, reaching: String) -> Dictionary:
102
+ ## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle. Answers `chain` as
103
+ ## well, each holder walked through and the step taken off it, for [method write_back].
104
+ static func walk_to(node: Object, node_path: String, reaching: String) -> Dictionary:
100
105
  var parts: PackedStringArray = steps_of(reaching)
101
106
  var holder: Variant = node
102
107
  var called: String = node_path
108
+ var chain: Array[Dictionary] = []
103
109
  for step: int in parts.size() - 1:
104
110
  var named: String = parts[step]
105
111
  if not can_read(holder, named):
106
112
  return {"type": "error", "message": nothing_there(holder, named, called)}
113
+ chain.append({"holder": holder, "named": named, "called": called})
107
114
  holder = read_under(holder, named)
108
115
  called = "%s:%s" % [called, named]
109
- if not _can_hold(holder):
116
+ if not can_hold(holder):
110
117
  var wanted: String = parts[step + 1]
111
- return {"type": "error", "message": "%s holds no object to read %s off" % [called, wanted]}
112
- return {"holder": holder, "name": parts[parts.size() - 1], "called": called}
118
+ return {
119
+ "type": "error",
120
+ "message":
121
+ (
122
+ "%s holds %s, which has nothing to read %s off"
123
+ % [called, type_string(typeof(holder)), wanted]
124
+ )
125
+ }
126
+ return {"holder": holder, "name": parts[parts.size() - 1], "called": called, "chain": chain}
113
127
 
114
128
 
115
129
  ## The object [param given] names for a method's argument, as `{"object": ...}`, or as
@@ -160,10 +174,17 @@ static func object_at(root: Node, node: Node, node_path: String, given: Variant)
160
174
  return {"object": value}
161
175
 
162
176
 
163
- ## Whether a step can be taken into [param value] at all: an object, a list or a map can be walked
164
- ## into, and a number or a string is where a path ends whether the caller meant it to or not.
165
- static func _can_hold(value: Variant) -> bool:
166
- return value is Object or value is Array or value is Dictionary or packed(value)
177
+ ## Whether a step can be taken into [param value] at all: an object, a list, a map or a value with
178
+ ## components can be walked into, and a number or a string is where a path ends whether the caller
179
+ ## meant it to or not.
180
+ static func can_hold(value: Variant) -> bool:
181
+ return value is Object or value is Array or value is Dictionary or packed(value) or composite(value)
182
+
183
+
184
+ ## Whether [param value] is read by its components, a vector or a colour or a transform, and is a
185
+ ## copy wherever it is read from.
186
+ static func composite(value: Variant) -> bool:
187
+ return not Values.components_of(value).is_empty()
167
188
 
168
189
 
169
190
  ## Whether [param value] is a packed list. Stepped into the way a list is, by index, and a write into
@@ -212,6 +233,8 @@ static func can_read(holder: Variant, named: String) -> bool:
212
233
  if not method.is_empty():
213
234
  return args.is_empty() and method in MAP_CALLS
214
235
  return not _key_in(map, named).is_empty()
236
+ if composite(holder):
237
+ return method.is_empty() and named in Values.components_of(holder)
215
238
  if holder is Object:
216
239
  var object: Object = holder
217
240
  if not method.is_empty():
@@ -420,25 +443,62 @@ static func _native_flags(object: Object, method: String) -> int:
420
443
  return 0
421
444
 
422
445
 
423
- ## Put [param value] where [param named] points. Only ever called once [method can_read] agrees,
424
- ## and lists and maps are reference types, so writing into one reaches the game's own copy.
425
- static func write(holder: Variant, named: String, value: Variant) -> void:
446
+ ## Put [param value] where [param named] points, and answer the holder written into. Only ever
447
+ ## called once [method can_read] agrees. Lists and maps are reference types, so writing into one
448
+ ## reaches the game's own copy; a vector or a transform is a copy, and the one answered is the
449
+ ## changed copy, for [method write_back] to put where it came from.
450
+ static func write(holder: Variant, named: String, value: Variant) -> Variant:
426
451
  if holder is Array:
427
452
  var items: Array = holder
428
453
  items[_index_in(named, items.size())] = value
429
- return
454
+ return holder
430
455
  if packed(holder):
431
456
  var list: Variant = holder
432
457
  list[_index_in(named, len(holder))] = value
433
- return
458
+ return holder
434
459
  if holder is Dictionary:
435
460
  var map: Dictionary = holder
436
461
  # Written back under the key it was found under, since a map keyed by StringName and one
437
462
  # keyed by String both read the same way and a write to the wrong one adds a second entry.
438
463
  map[_key_in(map, named)[0]] = value
439
- return
464
+ return holder
465
+ if composite(holder):
466
+ var changed: Variant = holder
467
+ changed[named] = value
468
+ return changed
440
469
  var object: Object = holder
441
470
  object.set(named, value)
471
+ return holder
472
+
473
+
474
+ ## Why a write into [param chain]'s last holder, a copy, cannot reach the game, or "" when it can:
475
+ ## each copy is put back where it was read from on the way up, and a step that was a call answered
476
+ ## a copy the game does not keep anywhere. [param chain] is the one [method walk_to] answered.
477
+ static func unwritable_copy(chain: Array[Dictionary]) -> String:
478
+ for index: int in range(chain.size() - 1, -1, -1):
479
+ var link: Dictionary = chain[index]
480
+ var named: String = link["named"]
481
+ if not method_of(named).is_empty():
482
+ return (
483
+ "%s:%s is what a call answered, a copy the game keeps nowhere, so a part of it is not a place to write"
484
+ % [link["called"], named]
485
+ )
486
+ if not composite(link["holder"]):
487
+ return ""
488
+ return ""
489
+
490
+
491
+ ## Puts [param changed], the copy a write into a vector or a transform changed, back where
492
+ ## [param chain] read it from, and every copy it was read out of back where that came from, up to
493
+ ## the first holder keeping its own: an object, whose setter runs, a list or a map.
494
+ static func write_back(chain: Array[Dictionary], changed: Variant) -> void:
495
+ var value: Variant = changed
496
+ for index: int in range(chain.size() - 1, -1, -1):
497
+ var link: Dictionary = chain[index]
498
+ var holder: Variant = link["holder"]
499
+ value = write(holder, str(link["named"]), value)
500
+ if not composite(holder):
501
+ return
442
502
 
443
503
 
444
504
  ## What [param holder] holds under [param named], or answers to it when [param named] is a call.
@@ -455,6 +515,8 @@ static func read_under(holder: Variant, named: String) -> Variant:
455
515
  if holder is Dictionary:
456
516
  var map: Dictionary = holder
457
517
  return map[_key_in(map, named)[0]]
518
+ if composite(holder):
519
+ return holder[named]
458
520
  var object: Object = holder
459
521
  var method: String = method_of(named)
460
522
  if not method.is_empty():
@@ -521,6 +583,18 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
521
583
  if keys.size() > 8:
522
584
  rest = " and %d more" % [keys.size() - 8]
523
585
  return "%s has no key %s; it is keyed by %s%s" % [called_as, named, ", ".join(some), rest]
586
+ if composite(holder):
587
+ var kind: String = type_string(typeof(holder))
588
+ var sort: String = ("an " if kind.left(1) in ["A", "E", "I", "O", "U"] else "a ") + kind
589
+ var has: String = ", ".join(Values.components_of(holder))
590
+ if not container_call.is_empty():
591
+ return (
592
+ "%s is %s, which a path reads by its components, %s, and makes no call on"
593
+ % [called_as, sort, has]
594
+ )
595
+ return "%s is %s, so it has no %s; it has %s" % [called_as, sort, named, has]
596
+ if not holder is Object:
597
+ return "%s is %s, which has nothing to read %s off" % [called_as, type_string(typeof(holder)), named]
524
598
  var object: Object = holder
525
599
  var method: String = method_of(named)
526
600
  if not method.is_empty():
@@ -634,12 +634,12 @@ func get_property(params: Dictionary) -> Dictionary:
634
634
  if node_path.is_empty() or property.is_empty():
635
635
  return {"type": "error", "message": "Node path and property required"}
636
636
 
637
- var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
637
+ var standing: Dictionary = Values.target_at(_host.get_tree().root, node_path)
638
638
  if standing.has("message"):
639
639
  return standing
640
- var node: Node = standing["node"]
640
+ var target: Object = standing["object"]
641
641
 
642
- var reached: Dictionary = Paths.walk_to(node, node_path, property)
642
+ var reached: Dictionary = Paths.walk_to(target, node_path, property)
643
643
  if reached.has("message"):
644
644
  return reached
645
645
 
@@ -11,6 +11,25 @@ extends RefCounted
11
11
  const Read = preload("reading.gd")
12
12
  const Serialisation = preload("serialisation.gd")
13
13
 
14
+ ## The engine singletons a call, a write or a read may name instead of a node: what the engine
15
+ ## measures, and the engine settings that are the game's own. Not every singleton: OS runs programs
16
+ ## and opens files, DisplayServer switches the keyboard's layout and Input moves the real pointer,
17
+ ## and none of that is a game's to do to the machine it runs on.
18
+ const SINGLETONS: Array[String] = [
19
+ "Engine",
20
+ "Performance",
21
+ "RenderingServer",
22
+ "AudioServer",
23
+ "PhysicsServer2D",
24
+ "PhysicsServer3D",
25
+ "NavigationServer2D",
26
+ "NavigationServer3D",
27
+ "Time",
28
+ "TranslationServer",
29
+ ]
30
+ ## The most frames a press is held in one call, as many as a frames wait takes.
31
+ const MOST_HELD: int = 600
32
+
14
33
  var _shared: Serialisation = Serialisation.new()
15
34
 
16
35
 
@@ -55,6 +74,56 @@ static func node_at(root: Node, path: String) -> Dictionary:
55
74
  return {"node": node}
56
75
 
57
76
 
77
+ ## How many process frames a whole press holds before its release, from `holdFrames`: one unless
78
+ ## asked, as the halves of a press have always been a frame apart. Held inside the game because the
79
+ ## time between a press, a wait and a release sent as three calls counts as held too: a 60-frame
80
+ ## pour poured for nearly three seconds. A String saying why, where the hold cannot be.
81
+ static func held_for(params: Dictionary) -> Variant:
82
+ if not params.has("holdFrames"):
83
+ return 1
84
+ if params.has("pressed"):
85
+ return (
86
+ "holdFrames holds a whole press and then lets go, and pressed asks for one half of one:"
87
+ + " give one or the other."
88
+ )
89
+ var frames: int = Read.as_int(params["holdFrames"], 0)
90
+ if frames < 1 or frames > MOST_HELD:
91
+ return "holdFrames is %d, and it takes 1 to %d." % [frames, MOST_HELD]
92
+ return frames
93
+
94
+
95
+ ## What a press says it was held for: [param frames] for a whole one, nothing for one half.
96
+ static func held_answer(whole: bool, frames: int) -> Variant:
97
+ if whole:
98
+ return frames
99
+ return null
100
+
101
+
102
+ ## What [param path] names: a node under [param root], as [method node_at] reads it, or an engine
103
+ ## singleton by its name. Answers the object under "object", and under "node" the node that objects
104
+ ## named in a value are found from, which for a singleton is [param root].
105
+ static func target_at(root: Node, path: String) -> Dictionary:
106
+ if SINGLETONS.has(path):
107
+ return {"object": Engine.get_singleton(path), "node": root}
108
+ if not path.contains("/") and Engine.has_singleton(path):
109
+ return {
110
+ "type": "error",
111
+ "message":
112
+ (
113
+ (
114
+ "%s is an engine singleton, and the ones that can be reached are %s. The rest act"
115
+ + " on the machine rather than the game: OS runs programs, DisplayServer switches the"
116
+ + " keyboard's layout and Input moves the real pointer."
117
+ )
118
+ % [path, ", ".join(SINGLETONS)]
119
+ ),
120
+ }
121
+ var standing: Dictionary = node_at(root, path)
122
+ if standing.has("message"):
123
+ return standing
124
+ return {"object": standing["node"], "node": standing["node"]}
125
+
126
+
58
127
  ## Whether two values can be compared without the comparison itself failing.
59
128
  static func comparable(one: Variant, other: Variant) -> bool:
60
129
  return Serialisation.comparable(one, other)
@@ -98,6 +167,52 @@ func parameter_class(object: Object, method: String, index: int) -> String:
98
167
  return ""
99
168
 
100
169
 
170
+ ## The components a path reads off [param value], by the names GDScript gives them, or none for a
171
+ ## value that has none: a vector's axes, a colour's channels, a rectangle's corners and size, a
172
+ ## transform's basis and origin. A value of one of these is a copy wherever it is read, so a write
173
+ ## into a component is a write into the copy until it is put back where it was read from.
174
+ static func components_of(value: Variant) -> PackedStringArray:
175
+ match typeof(value):
176
+ TYPE_VECTOR2, TYPE_VECTOR2I:
177
+ return PackedStringArray(["x", "y"])
178
+ TYPE_VECTOR3, TYPE_VECTOR3I:
179
+ return PackedStringArray(["x", "y", "z"])
180
+ TYPE_VECTOR4, TYPE_VECTOR4I, TYPE_QUATERNION:
181
+ return PackedStringArray(["x", "y", "z", "w"])
182
+ TYPE_COLOR:
183
+ return PackedStringArray(
184
+ [
185
+ "r",
186
+ "g",
187
+ "b",
188
+ "a",
189
+ "r8",
190
+ "g8",
191
+ "b8",
192
+ "a8",
193
+ "h",
194
+ "s",
195
+ "v",
196
+ "ok_hsl_h",
197
+ "ok_hsl_s",
198
+ "ok_hsl_l"
199
+ ]
200
+ )
201
+ TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
202
+ return PackedStringArray(["position", "size", "end"])
203
+ TYPE_PLANE:
204
+ return PackedStringArray(["normal", "x", "y", "z", "d"])
205
+ TYPE_BASIS:
206
+ return PackedStringArray(["x", "y", "z"])
207
+ TYPE_TRANSFORM2D:
208
+ return PackedStringArray(["x", "y", "origin"])
209
+ TYPE_TRANSFORM3D:
210
+ return PackedStringArray(["basis", "origin"])
211
+ TYPE_PROJECTION:
212
+ return PackedStringArray(["x", "y", "z", "w"])
213
+ return PackedStringArray()
214
+
215
+
101
216
  ## What the slot [param named] on [param holder] declares it holds, as `{"type": int, "class":
102
217
  ## String, "script": Script or null}`: a property's declaration, or the element type of a typed list
103
218
  ## or the value type of a typed map. TYPE_NIL when nothing is declared, which is also what a
@@ -111,6 +226,11 @@ func parameter_class(object: Object, method: String, index: int) -> String:
111
226
  ## as a script class with no `class_name` is declared under the engine class the script extends,
112
227
  ## measured on 4.7.2: `var quarrel: Walker` with Walker a preloaded Node3D script lists as Node3D.
113
228
  static func slot_declared(holder: Variant, named: String) -> Dictionary:
229
+ # A component holds one kind of value and no other: a vector's axis is a number, its integer
230
+ # form's an integer, a rectangle's size a vector of the same form.
231
+ if named in components_of(holder):
232
+ var component: Variant = holder[named]
233
+ return {"type": typeof(component), "class": "", "script": null}
114
234
  if holder is Array:
115
235
  var items: Array = holder
116
236
  if items.is_typed():