gdharness 1.2.28 → 1.2.30

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.
@@ -32,15 +32,16 @@ func set_property(params: Dictionary) -> Dictionary:
32
32
  if node_path.is_empty() or property.is_empty():
33
33
  return {"type": "error", "message": "Node path and property required"}
34
34
 
35
- var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
35
+ var standing: Dictionary = Values.target_at(_host.get_tree().root, node_path)
36
36
  if standing.has("message"):
37
37
  return standing
38
38
  var node: Node = standing["node"]
39
+ var target: Object = standing["object"]
39
40
 
40
41
  # Through a path as well, for the reason [method Paths.walk_to] gives, and read back off the same
41
42
  # holder afterwards: a set that does not take says so by answering with the old value, which is
42
43
  # how a typed container refusing a write is told from one accepting it.
43
- var reached: Dictionary = Paths.walk_to(node, node_path, property)
44
+ var reached: Dictionary = Paths.walk_to(target, node_path, property)
44
45
  if reached.has("message"):
45
46
  return reached
46
47
 
@@ -60,70 +61,35 @@ func set_property(params: Dictionary) -> Dictionary:
60
61
  if not Paths.can_read(holder, named):
61
62
  return {"type": "error", "message": Paths.nothing_there(holder, named, str(reached["called"]))}
62
63
  var old_value: Variant = Paths.read_under(holder, named)
63
- # The same rule the call path holds: a value that cannot become what the property holds is
64
- # refused rather than written. Writing it means the engine picks something, and what it picks
65
- # for a word where a number goes is zero, which the answer then reports as the new value.
66
- #
67
- # A slot declaring nothing, an untyped or Variant property or an element of an untyped list, takes
68
- # anything, and the engine stores what it is given there. What it holds now still says how a value
69
- # of the same kind is meant, 5 into an int staying an int, and whether a path names an object, but
70
- # a value of another kind is stored as sent: a word into one holding a number is the word, and a
71
- # number into one holding a word is the number, not its text. The text null empties it when it
72
- # holds an object or nothing, as a wait reads it on the same slot.
73
64
  var slot: Dictionary = Values.slot_declared(holder, named)
74
65
  var declared: int = slot["type"]
75
66
  var declared_script: Script = slot["script"]
76
- var wanted: int = declared if declared != TYPE_NIL else typeof(old_value)
77
- var holds_objects: bool = (
78
- declared == TYPE_OBJECT or (declared == TYPE_NIL and (old_value == null or old_value is Object))
79
- )
80
- var given: Variant
81
- if holds_objects and names_no_object(value):
82
- given = null
83
- elif wanted == TYPE_OBJECT and (value is String or value is Dictionary):
84
- var named_object: Dictionary = _object_named(
85
- node, node_path, value, str(slot["class"]), declared_script
86
- )
87
- if named_object.has("message"):
88
- return {
89
- "type": "error",
90
- "message": "%s.%s holds an object: %s" % [reached["called"], named, named_object["message"]]
91
- }
92
- given = named_object["object"]
93
- else:
94
- var as_sent: Variant = _values.fitted(value, TYPE_NIL)
95
- if declared == TYPE_NIL and not Values.acceptable(as_sent, wanted):
96
- given = as_sent
97
- else:
98
- given = _values.fitted(value, wanted)
99
- if not Values.acceptable(given, wanted):
100
- return {
101
- "type": "error",
102
- "message":
103
- (
104
- "%s.%s holds %s and the value given is %s, which cannot become one.%s"
105
- % [
106
- reached["called"],
107
- named,
108
- type_string(wanted),
109
- type_string(typeof(given)),
110
- _written_as(wanted)
111
- ]
112
- )
113
- }
114
- var typed: Dictionary = _values.typed_like(given, old_value, _element_object.bind(node, node_path))
115
- if typed.has("message"):
116
- return {
117
- "type": "error",
118
- "message":
119
- "%s.%s is a typed %s: %s." % [reached["called"], named, _sort_of(old_value), typed["message"]]
120
- }
121
- given = typed["value"]
67
+ var fitting: Dictionary = _value_for(value, slot, old_value, reached, node, node_path)
68
+ if fitting.has("message"):
69
+ return fitting
70
+ var given: Variant = fitting["value"]
71
+ # A component of a vector or a transform is written into a copy, which is put back where it was
72
+ # read from, setter and all; the read back is then taken off the game again, since the copy here
73
+ # says only what was written into it.
74
+ var chain: Array[Dictionary] = reached["chain"]
75
+ var copied: bool = Paths.composite(holder)
76
+ if copied:
77
+ var unwritable: String = Paths.unwritable_copy(chain)
78
+ if not unwritable.is_empty():
79
+ return {"type": "error", "message": unwritable}
122
80
  # The write alone, setter included, which is where a property's cost is.
123
81
  var began: int = Time.get_ticks_usec()
124
- Paths.write(holder, named, given)
82
+ var written: Variant = Paths.write(holder, named, given)
83
+ if copied:
84
+ Paths.write_back(chain, written)
125
85
  var elapsed: int = Time.get_ticks_usec() - began
126
86
  var now: Variant = Paths.read_under(holder, named)
87
+ if copied:
88
+ var again: Dictionary = Paths.walk_to(target, node_path, property)
89
+ if again.has("message"):
90
+ return again
91
+ var reread: Variant = again["holder"]
92
+ now = Paths.read_under(reread, named) if Paths.can_read(reread, named) else null
127
93
  # Read back rather than trusted. The engine drops a write it will not take without a word, and
128
94
  # the answer then showed the old value as the new one, shaped as a success: a typed container
129
95
  # refusing a plain list did exactly that. A value the property changed on the way in, a setter
@@ -183,6 +149,131 @@ func set_property(params: Dictionary) -> Dictionary:
183
149
  return answer
184
150
 
185
151
 
152
+ ## What [param sent] becomes as argument [param index] of the method [param named] on the holder
153
+ ## [param reached] ends at, as [method Paths.walk_to] answered it, as `{"value": ...}`, or the refusal
154
+ ## saying why it cannot become one: a number the parameter would have to cut, a value that cannot be
155
+ ## converted, or a list or map that cannot be built as the typed one the parameter takes.
156
+ func _argument_for(
157
+ sent: Variant, index: int, reached: Dictionary, named: String, node: Node, node_path: String
158
+ ) -> Dictionary:
159
+ var holder: Object = reached["holder"]
160
+ var called: String = reached["called"]
161
+ var wants: int = _values.parameter_type(holder, named, index)
162
+ if Values.cuts(sent, wants):
163
+ return {
164
+ "type": "error",
165
+ "message":
166
+ (
167
+ "%s.%s takes %s as argument %d and was given %s, which it would have to be cut to fit"
168
+ % [called, named, type_string(wants), index + 1, JSON.stringify(sent)]
169
+ )
170
+ }
171
+ var given: Variant = _values.fitted(sent, wants)
172
+ if not Values.acceptable(given, wants):
173
+ return {
174
+ "type": "error",
175
+ "message":
176
+ (
177
+ "%s.%s takes %s as argument %d and was given %s, which cannot be converted.%s"
178
+ % [
179
+ called,
180
+ named,
181
+ type_string(wants),
182
+ index + 1,
183
+ type_string(typeof(given)),
184
+ _written_as(wants)
185
+ ]
186
+ )
187
+ }
188
+ # A typed list or map is built as one, since a plain one handed to such a parameter raises
189
+ # inside the game, which is what every check here exists to stop.
190
+ var container: Variant = _values.parameter_container(holder, named, index)
191
+ if container == null:
192
+ return {"value": given}
193
+ var typed: Dictionary = _values.typed_like(given, container, _element_object.bind(node, node_path))
194
+ if typed.has("message"):
195
+ return {
196
+ "type": "error",
197
+ "message":
198
+ (
199
+ "%s.%s takes a typed %s as argument %d: %s."
200
+ % [called, named, _sort_of(container), index + 1, typed["message"]]
201
+ )
202
+ }
203
+ return {"value": typed["value"]}
204
+
205
+
206
+ ## What [param value] becomes written into the slot [param reached] ends on, as
207
+ ## [method Paths.walk_to] answered it, as `{"value": ...}`, or the refusal saying why it cannot become
208
+ ## one.
209
+ ##
210
+ ## The same rule the call path holds: a value that cannot become what the property holds is
211
+ ## refused rather than written. Writing it means the engine picks something, and what it picks
212
+ ## for a word where a number goes is zero, which the answer then reports as the new value. So is a
213
+ ## number a whole number or a truth would have to cut, since converting one truncates.
214
+ ##
215
+ ## A slot declaring nothing, an untyped or Variant property or an element of an untyped list, takes
216
+ ## anything, and the engine stores what it is given there. What it holds now still says how a value
217
+ ## of the same kind is meant, 5 into an int staying an int, and whether a path names an object, but
218
+ ## a value of another kind is stored as sent: a word into one holding a number is the word, a
219
+ ## fraction into one holding a whole number is the fraction, and a number into one holding a word is
220
+ ## the number, not its text. The text null empties it when it holds an object or nothing, as a wait
221
+ ## reads it on the same slot.
222
+ func _value_for(
223
+ value: Variant, slot: Dictionary, old_value: Variant, reached: Dictionary, node: Node, node_path: String
224
+ ) -> Dictionary:
225
+ var named: String = reached["name"]
226
+ var called: String = reached["called"]
227
+ var declared: int = slot["type"]
228
+ var declared_script: Script = slot["script"]
229
+ var wanted: int = declared if declared != TYPE_NIL else typeof(old_value)
230
+ var holds_objects: bool = (
231
+ declared == TYPE_OBJECT or (declared == TYPE_NIL and (old_value == null or old_value is Object))
232
+ )
233
+ if holds_objects and names_no_object(value):
234
+ return {"value": null}
235
+ if wanted == TYPE_OBJECT and (value is String or value is Dictionary):
236
+ var named_object: Dictionary = _object_named(
237
+ node, node_path, value, str(slot["class"]), declared_script
238
+ )
239
+ if named_object.has("message"):
240
+ return {
241
+ "type": "error",
242
+ "message": "%s.%s holds an object: %s" % [called, named, named_object["message"]]
243
+ }
244
+ return {"value": named_object["object"]}
245
+ var as_sent: Variant = _values.fitted(value, TYPE_NIL)
246
+ var cut: bool = Values.cuts(value, wanted)
247
+ var given: Variant = as_sent
248
+ if declared != TYPE_NIL and cut:
249
+ return {
250
+ "type": "error",
251
+ "message":
252
+ (
253
+ "%s.%s holds %s and the value given is %s, which it would have to be cut to fit"
254
+ % [called, named, type_string(wanted), JSON.stringify(value)]
255
+ )
256
+ }
257
+ if declared != TYPE_NIL or (not cut and Values.acceptable(as_sent, wanted)):
258
+ given = _values.fitted(value, wanted)
259
+ if not Values.acceptable(given, wanted):
260
+ return {
261
+ "type": "error",
262
+ "message":
263
+ (
264
+ "%s.%s holds %s and the value given is %s, which cannot become one.%s"
265
+ % [called, named, type_string(wanted), type_string(typeof(given)), _written_as(wanted)]
266
+ )
267
+ }
268
+ var typed: Dictionary = _values.typed_like(given, old_value, _element_object.bind(node, node_path))
269
+ if typed.has("message"):
270
+ return {
271
+ "type": "error",
272
+ "message": "%s.%s is a typed %s: %s." % [called, named, _sort_of(old_value), typed["message"]]
273
+ }
274
+ return {"value": typed["value"]}
275
+
276
+
186
277
  ## Whether [param given] is the value [param held] already is, at the precision the property keeps.
187
278
  ##
188
279
  ## A float arrives as 64 bits and most engine properties keep 32: Camera3D.h_offset given -1.1
@@ -211,15 +302,16 @@ func call_method(params: Dictionary) -> Dictionary:
211
302
  if node_path.is_empty() or method.is_empty():
212
303
  return {"type": "error", "message": "Node path and method required"}
213
304
 
214
- var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
305
+ var standing: Dictionary = Values.target_at(_host.get_tree().root, node_path)
215
306
  if standing.has("message"):
216
307
  return standing
217
308
  var node: Node = standing["node"]
309
+ var target: Object = standing["object"]
218
310
 
219
311
  # Through a path as well, for the reason [method Paths.walk_to] gives. What a game does hangs off its
220
312
  # nodes as much as its state does, so reading `_game:run:day` while being unable to call
221
313
  # `_game:run:advance` answers half of what a node holds and refuses the other half.
222
- var reached: Dictionary = Paths.walk_to(node, node_path, method)
314
+ var reached: Dictionary = Paths.walk_to(target, node_path, method)
223
315
  if reached.has("message"):
224
316
  return reached
225
317
 
@@ -235,6 +327,12 @@ func call_method(params: Dictionary) -> Dictionary:
235
327
  var spelled: String = str(reached["name"])
236
328
  if Paths.method_of(spelled).is_empty():
237
329
  spelled += "()"
330
+ # A vector or a transform has no element to call a method on: it is read by its components.
331
+ if Paths.composite(reached["holder"]):
332
+ return {
333
+ "type": "error",
334
+ "message": Paths.nothing_there(reached["holder"], spelled, str(reached["called"]))
335
+ }
238
336
  if args.is_empty() and Paths.can_read(reached["holder"], spelled):
239
337
  var read_from: int = Time.get_ticks_usec()
240
338
  var answered: Variant = Paths.read_under(reached["holder"], spelled)
@@ -301,41 +399,10 @@ func call_method(params: Dictionary) -> Dictionary:
301
399
  by_engine_class[index + 1] = [declared, Values.class_of(resolved)]
302
400
  deserialized_args.append(named_object["object"])
303
401
  continue
304
- var given: Variant = _values.fitted(args[index], wants)
305
- if not Values.acceptable(given, wants):
306
- return {
307
- "type": "error",
308
- "message":
309
- (
310
- "%s.%s takes %s as argument %d and was given %s, which cannot be converted.%s"
311
- % [
312
- reached["called"],
313
- named,
314
- type_string(wants),
315
- index + 1,
316
- type_string(typeof(given)),
317
- _written_as(wants)
318
- ]
319
- )
320
- }
321
- # A typed list or map is built as one, since a plain one handed to such a parameter raises
322
- # inside the game, which is what every check here exists to stop.
323
- var container: Variant = _values.parameter_container(holder, named, index)
324
- if container != null:
325
- var typed: Dictionary = _values.typed_like(
326
- given, container, _element_object.bind(node, node_path)
327
- )
328
- if typed.has("message"):
329
- return {
330
- "type": "error",
331
- "message":
332
- (
333
- "%s.%s takes a typed %s as argument %d: %s."
334
- % [reached["called"], named, _sort_of(container), index + 1, typed["message"]]
335
- )
336
- }
337
- given = typed["value"]
338
- deserialized_args.append(given)
402
+ var argument: Dictionary = _argument_for(args[index], index, reached, named, node, node_path)
403
+ if argument.has("message"):
404
+ return argument
405
+ deserialized_args.append(argument["value"])
339
406
 
340
407
  # Timed around the call alone, since the bridge's round trip is a second or more and swamps what
341
408
  # one click's work costs; the frame metrics read whichever frame is last, which is not this one.
@@ -461,8 +528,11 @@ static func _walk_from(object: Object, path: String) -> Dictionary:
461
528
  var holder: Variant = object
462
529
  var called: String = "the result"
463
530
  for named: String in Paths.steps_of(path):
464
- if not (holder is Object or holder is Array or holder is Dictionary or Paths.packed(holder)):
465
- return {"message": "%s holds no object to read %s off" % [called, named]}
531
+ if not Paths.can_hold(holder):
532
+ return {
533
+ "message":
534
+ "%s holds %s, which has nothing to read %s off" % [called, type_string(typeof(holder)), named]
535
+ }
466
536
  if not Paths.can_read(holder, named):
467
537
  return {"message": Paths.nothing_there(holder, named, called)}
468
538
  holder = Paths.read_under(holder, named)
@@ -122,12 +122,16 @@ func inject_action(params: Dictionary) -> Dictionary:
122
122
 
123
123
  if not InputMap.has_action(action):
124
124
  return {"type": "error", "message": "Action not found: " + action}
125
+ var holding: Variant = Values.held_for(params)
126
+ if holding is String:
127
+ return {"type": "error", "message": holding}
128
+ var frames: int = holding
125
129
 
126
130
  _say_action(action, whole or held, strength)
127
131
  if whole:
128
- # A frame between the halves, as a click has: a listener that acts on the press and a
129
- # listener that acts on the release both get their own frame to do it in.
130
- await _host.get_tree().process_frame
132
+ # A frame between the halves at least, as a click has: a listener that acts on the press and
133
+ # a listener that acts on the release both get their own frame to do it in.
134
+ await _held(frames)
131
135
  _say_action(action, false, strength)
132
136
  await _host.get_tree().process_frame
133
137
 
@@ -137,9 +141,16 @@ func inject_action(params: Dictionary) -> Dictionary:
137
141
  "action": action,
138
142
  "pressed": held and not whole,
139
143
  "whole": whole,
144
+ "held_frames": Values.held_answer(whole, frames),
140
145
  }
141
146
 
142
147
 
148
+ ## Waits out [param frames] process frames, which is how long a whole press is held down.
149
+ func _held(frames: int) -> void:
150
+ for _frame: int in frames:
151
+ await _host.get_tree().process_frame
152
+
153
+
143
154
  func _say_action(action: String, down: bool, strength: float) -> void:
144
155
  var event: InputEventAction = InputEventAction.new()
145
156
  event.action = action
@@ -160,6 +171,10 @@ func inject_key(params: Dictionary) -> Dictionary:
160
171
  if not named.is_empty() and key_label.is_empty():
161
172
  key_label = named
162
173
  var keycode: int = 0 if keycode_raw is String else Read.as_int(keycode_raw)
174
+ var holding: Variant = Values.held_for(params)
175
+ if holding is String:
176
+ return {"type": "error", "message": holding}
177
+ var frames: int = holding
163
178
 
164
179
  var event: InputEventKey = InputEventKey.new()
165
180
  event.pressed = whole or held
@@ -193,7 +208,7 @@ func inject_key(params: Dictionary) -> Dictionary:
193
208
 
194
209
  Input.parse_input_event(event)
195
210
  if whole:
196
- await _host.get_tree().process_frame
211
+ await _held(frames)
197
212
  var up: InputEventKey = event.duplicate()
198
213
  up.pressed = false
199
214
  Input.parse_input_event(up)
@@ -202,6 +217,7 @@ func inject_key(params: Dictionary) -> Dictionary:
202
217
  return {
203
218
  "type": "input_injected",
204
219
  "input_type": "key",
220
+ "held_frames": Values.held_answer(whole, frames),
205
221
  "keycode": event.keycode,
206
222
  "physical_keycode": event.physical_keycode,
207
223
  "shift": event.shift_pressed,
@@ -266,6 +282,15 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
266
282
  # than under the pointer, landing true and doing nothing, until a release was sent by hand.
267
283
  var notch: bool = held and _mask_of(button) == 0
268
284
  var whole: bool = notch or not params.has("pressed")
285
+ var holding: Variant = Values.held_for(params)
286
+ if holding is String:
287
+ return {"type": "error", "message": holding}
288
+ var frames: int = holding
289
+ if notch and params.has("holdFrames"):
290
+ return {
291
+ "type": "error",
292
+ "message": "A wheel step is pressed and let go at once, so holdFrames cannot hold one."
293
+ }
269
294
 
270
295
  # A button pressed somewhere puts the pointer there: the motion after a held press measures
271
296
  # its drag from the press, which is where a real pointer would be.
@@ -274,9 +299,9 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
274
299
  if notch:
275
300
  Input.parse_input_event(_button(position, button, false, false))
276
301
  elif whole:
277
- # A frame between the halves, as inject_action has, so a control that acts on the press and
278
- # one that acts on the release each get a frame to do it in.
279
- await _host.get_tree().process_frame
302
+ # A frame between the halves at least, as inject_action has, so a control that acts on the
303
+ # press and one that acts on the release each get a frame to do it in.
304
+ await _held(frames)
280
305
  Input.parse_input_event(_button(position, button, false, false))
281
306
  await _host.get_tree().process_frame
282
307
 
@@ -284,6 +309,7 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
284
309
  {
285
310
  "type": "input_injected",
286
311
  "input_type": "mouse_click",
312
+ "held_frames": Values.held_answer(whole and not notch, frames),
287
313
  "position": [position.x, position.y],
288
314
  "button": button,
289
315
  "pressed": held and not whole,
@@ -9,6 +9,7 @@ extends RefCounted
9
9
  ## calling the game's own methods. The engine still fires the actions bound to a button from these.
10
10
 
11
11
  const Read = preload("reading.gd")
12
+ const Values = preload("runtime_values.gd")
12
13
 
13
14
  ## Controller buttons by the names Godot gives them, for a caller naming one rather than numbering it.
14
15
  const JOY_BUTTONS: Dictionary[String, int] = {
@@ -62,18 +63,24 @@ func inject_joypad_button(params: Dictionary) -> Dictionary:
62
63
  var device: int = Read.as_int(params.get("device", 0), 0)
63
64
  var held: bool = Read.as_bool(params.get("pressed", true), true)
64
65
  var whole: bool = not params.has("pressed")
66
+ var holding: Variant = Values.held_for(params)
67
+ if holding is String:
68
+ return {"type": "error", "message": holding}
69
+ var frames: int = holding
65
70
 
66
71
  Input.parse_input_event(_button(button, device, whole or held))
67
72
  if whole:
68
- # A frame between the halves, as a key has, so a game that acts on the press and one that
69
- # acts on the release each get a frame to do it in.
70
- await _host.get_tree().process_frame
73
+ # A frame between the halves at least, as a key has, so a game that acts on the press and one
74
+ # that acts on the release each get a frame to do it in.
75
+ for _frame: int in frames:
76
+ await _host.get_tree().process_frame
71
77
  Input.parse_input_event(_button(button, device, false))
72
78
  await _host.get_tree().process_frame
73
79
 
74
80
  return {
75
81
  "type": "input_injected",
76
82
  "input_type": "joypad_button",
83
+ "held_frames": Values.held_answer(whole, frames),
77
84
  "button": button,
78
85
  "button_name": _name_of(button, JOY_BUTTONS),
79
86
  "device": device,
@@ -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