gdharness 1.2.27 → 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():
@@ -35,7 +35,10 @@ var _host: Node
35
35
  var _values: Values
36
36
  ## How long each of the last frames took, oldest first, in microseconds, as the game timed them.
37
37
  var _frame_usec: PackedInt64Array = PackedInt64Array()
38
+ ## How many physics steps ran in each of those frames, in the same order.
39
+ var _frame_physics: PackedInt64Array = PackedInt64Array()
38
40
  var _last_frame_usec: int = 0
41
+ var _last_physics_frames: int = 0
39
42
 
40
43
 
41
44
  ## The host is the autoload, which is how the tree is reached: it is not in one when the
@@ -631,12 +634,12 @@ func get_property(params: Dictionary) -> Dictionary:
631
634
  if node_path.is_empty() or property.is_empty():
632
635
  return {"type": "error", "message": "Node path and property required"}
633
636
 
634
- 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)
635
638
  if standing.has("message"):
636
639
  return standing
637
- var node: Node = standing["node"]
640
+ var target: Object = standing["object"]
638
641
 
639
- var reached: Dictionary = Paths.walk_to(node, node_path, property)
642
+ var reached: Dictionary = Paths.walk_to(target, node_path, property)
640
643
  if reached.has("message"):
641
644
  return reached
642
645
 
@@ -662,12 +665,21 @@ func get_property(params: Dictionary) -> Dictionary:
662
665
  ## read 45ms for a second at a time, and a change to the game showed only on the next second's
663
666
  ## worst frame. The time between two of these calls is the frame as it passed, physics and the wait
664
667
  ## for the display included.
668
+ ##
669
+ ## The physics steps are counted beside it because the engine steps physics from the wall clock and
670
+ ## nothing else reports how many it took: a game drawing sixty frames a second whose brew advanced
671
+ ## by physics steps moved a tenth as far as the frames promised, and every reading available said
672
+ ## the game was running well.
665
673
  func note_frame(now_usec: int) -> void:
674
+ var physics: int = Engine.get_physics_frames()
666
675
  if _last_frame_usec > 0:
667
676
  var _grew: bool = _frame_usec.push_back(now_usec - _last_frame_usec)
677
+ var _stepped: bool = _frame_physics.push_back(physics - _last_physics_frames)
668
678
  if _frame_usec.size() > FRAMES_TIMED:
669
679
  _frame_usec.remove_at(0)
680
+ _frame_physics.remove_at(0)
670
681
  _last_frame_usec = now_usec
682
+ _last_physics_frames = physics
671
683
 
672
684
 
673
685
  ## Whether the frame waits for the display, which sets its floor: a game with vsync on cannot
@@ -695,16 +707,26 @@ func get_metrics(params: Dictionary) -> Dictionary:
695
707
  total_usec += one
696
708
  slowest_usec = maxi(slowest_usec, one)
697
709
  # Null rather than zero before a frame has been timed: no frame took no time.
710
+ var steps: int = 0
711
+ for one: int in _frame_physics:
712
+ steps += one
698
713
  var frame_time: Variant = null
699
714
  var frame_time_max: Variant = null
715
+ var physics_steps_per_second: Variant = null
700
716
  if timed > 0:
701
717
  frame_time = float(total_usec) / timed / 1000000.0
702
718
  frame_time_max = slowest_usec / 1000000.0
719
+ if total_usec > 0:
720
+ physics_steps_per_second = steps * 1000000.0 / total_usec
703
721
  var all: Dictionary = {
704
722
  "fps": Engine.get_frames_per_second(),
705
723
  "frame_time": frame_time,
706
724
  "frame_time_max": frame_time_max,
707
725
  "frames_timed": timed,
726
+ "physics_steps_per_second": physics_steps_per_second,
727
+ "physics_ticks_per_second": Engine.physics_ticks_per_second,
728
+ "max_physics_steps_per_frame": Engine.max_physics_steps_per_frame,
729
+ "time_scale": Engine.time_scale,
708
730
  "process_time_max": Performance.get_monitor(Performance.TIME_PROCESS),
709
731
  "physics_time": Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS),
710
732
  "vsync": _vsync(),
@@ -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():