gdharness 1.3.8 → 1.3.10
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.
- package/build/cli.js +4668 -3229
- package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
- package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
- package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
- package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
- package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
- package/build/godot/addons/gdharness_editor/reading.gd +112 -6
- package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
- package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
- package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
- package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
- package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
- package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
- package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
- package/build/godot/operations/animation_reads.gd +115 -4
- package/build/godot/operations/audio_buses.gd +93 -7
- package/build/godot/operations/classdb_queries.gd +119 -7
- package/build/godot/operations/declared_values.gd +272 -0
- package/build/godot/operations/dependencies.gd +154 -41
- package/build/godot/operations/editor_tools.gd +29 -13
- package/build/godot/operations/export_presets.gd +602 -4
- package/build/godot/operations/file_reads.gd +115 -18
- package/build/godot/operations/gdscript_analysis.gd +14 -0
- package/build/godot/operations/gdscript_authoring.gd +12 -2
- package/build/godot/operations/godot_operations.gd +18 -2
- package/build/godot/operations/import_pipeline.gd +20 -221
- package/build/godot/operations/input_actions.gd +127 -51
- package/build/godot/operations/project_config.gd +113 -68
- package/build/godot/operations/project_diagnostics.gd +4 -0
- package/build/godot/operations/project_validation.gd +266 -0
- package/build/godot/operations/property_paths.gd +89 -16
- package/build/godot/operations/property_values.gd +180 -121
- package/build/godot/operations/reading.gd +112 -6
- package/build/godot/operations/scene_connections.gd +222 -0
- package/build/godot/operations/serialisation.gd +19 -29
- package/build/godot/operations/setting_reads.gd +121 -0
- package/build/godot/operations/value_shapes.gd +128 -1
- package/build/godot/tests/listed.gd +20 -13
- package/build/keeper.js +1402 -707
- package/package.json +1 -1
|
@@ -27,12 +27,40 @@ const LOOKS_APART: int = 9
|
|
|
27
27
|
## two looks and a wait for 32 never sees it.
|
|
28
28
|
const COMPARES: Array[String] = ["==", "!=", ">=", "<=", ">", "<"]
|
|
29
29
|
|
|
30
|
-
## The most signal watches kept at once; arming one past this lets the oldest go.
|
|
30
|
+
## The most signal watches kept at once; arming one past this lets the oldest go. Property watches
|
|
31
|
+
## are counted apart, to the same number.
|
|
31
32
|
const MOST_WATCHES: int = 16
|
|
32
33
|
|
|
34
|
+
## The most changes a property watch keeps. Past it, each new one takes the last place, so what is
|
|
35
|
+
## kept still ends on the latest value.
|
|
36
|
+
const MOST_CHANGES: int = 1000
|
|
37
|
+
|
|
38
|
+
## The commands that can change the game, by the tool call that sends each, which a property watch
|
|
39
|
+
## notes as they arrive.
|
|
40
|
+
const CALLS_NAMED: Dictionary[String, String] = {
|
|
41
|
+
"set_property": "runtime_invoke set",
|
|
42
|
+
"call_method": "runtime_invoke call",
|
|
43
|
+
"click": "runtime_input click",
|
|
44
|
+
"choose": "runtime_input choose",
|
|
45
|
+
"inject_action": "runtime_input action",
|
|
46
|
+
"inject_key": "runtime_input key",
|
|
47
|
+
"inject_text": "runtime_input text",
|
|
48
|
+
"inject_mouse_click": "runtime_input mouse_click",
|
|
49
|
+
"inject_mouse_motion": "runtime_input mouse_motion",
|
|
50
|
+
"inject_mouse_drag": "runtime_input mouse_drag",
|
|
51
|
+
"inject_joypad_button": "runtime_input joypad_button",
|
|
52
|
+
"inject_joypad_motion": "runtime_input joypad_motion",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
## What a call noted by a property watch carries of its parameters, when it was given them.
|
|
56
|
+
const CALL_FIELDS: Array[String] = [
|
|
57
|
+
"path", "method", "property", "action", "keycode", "button", "pressed", "says"
|
|
58
|
+
]
|
|
59
|
+
|
|
33
60
|
var _host: Node
|
|
34
61
|
var _values: GdhValues
|
|
35
62
|
var _watches: Dictionary[int, GdhWatch] = {}
|
|
63
|
+
var _traces: Dictionary[int, GdhTrace] = {}
|
|
36
64
|
var _next_watch: int = 1
|
|
37
65
|
|
|
38
66
|
|
|
@@ -110,9 +138,10 @@ func wait_signal(params: Dictionary) -> Dictionary:
|
|
|
110
138
|
}
|
|
111
139
|
if not catcher.fired:
|
|
112
140
|
answer["note"] = (
|
|
113
|
-
"
|
|
114
|
-
+ "
|
|
115
|
-
+ "
|
|
141
|
+
"A wait hears the signal only from when it begins, so one that fired before it is not"
|
|
142
|
+
+ " counted. If a call meant to set the signal off was sent beside this wait, it may have run"
|
|
143
|
+
+ " only after the wait answered, as a client can run its calls in turn: arm runtime_wait"
|
|
144
|
+
+ " watch first, make the call, then wait with signal and that watch"
|
|
116
145
|
)
|
|
117
146
|
return answer
|
|
118
147
|
|
|
@@ -125,6 +154,18 @@ func wait_signal(params: Dictionary) -> Dictionary:
|
|
|
125
154
|
func watch_signal(params: Dictionary) -> Dictionary:
|
|
126
155
|
var node_path: String = str(params.get("path", ""))
|
|
127
156
|
var signal_name: String = str(params.get("signal", ""))
|
|
157
|
+
var property: String = str(params.get("property", ""))
|
|
158
|
+
if not signal_name.is_empty() and not property.is_empty():
|
|
159
|
+
return {
|
|
160
|
+
"type": "error",
|
|
161
|
+
"message":
|
|
162
|
+
(
|
|
163
|
+
"A watch records a signal or a property, not both: this one has signal %s and property %s"
|
|
164
|
+
% [signal_name, property]
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
if not property.is_empty():
|
|
168
|
+
return _watch_property(node_path, property)
|
|
128
169
|
if node_path.is_empty() or signal_name.is_empty():
|
|
129
170
|
return {"type": "error", "message": "Node path and signal name required"}
|
|
130
171
|
var standing: Dictionary = GdhValues.node_at(_host.get_tree().root, node_path)
|
|
@@ -149,6 +190,15 @@ func watch_signal(params: Dictionary) -> Dictionary:
|
|
|
149
190
|
## Answers at once with the first emission recorded since watch [param id] was armed, otherwise
|
|
150
191
|
## waits for the next until the time runs out. The watch goes either way.
|
|
151
192
|
func _collect(id: int, timeout_ms: int) -> Dictionary:
|
|
193
|
+
if _traces.has(id):
|
|
194
|
+
return {
|
|
195
|
+
"type": "error",
|
|
196
|
+
"message":
|
|
197
|
+
(
|
|
198
|
+
"Watch %d records %s:%s, a property, and is collected with runtime_wait recorded"
|
|
199
|
+
% [id, _traces[id].node_path, _traces[id].property]
|
|
200
|
+
)
|
|
201
|
+
}
|
|
152
202
|
if not _watches.has(id):
|
|
153
203
|
return {
|
|
154
204
|
"type": "error",
|
|
@@ -194,6 +244,153 @@ func _let_go(id: int) -> void:
|
|
|
194
244
|
node.disconnect(watch.signal_name, callable)
|
|
195
245
|
|
|
196
246
|
|
|
247
|
+
## Starts recording [param property] every frame and answers at once with the watch and what it
|
|
248
|
+
## reads now.
|
|
249
|
+
##
|
|
250
|
+
## A tween or an animation is over before a client's next call reaches the game: a slide of 0.3
|
|
251
|
+
## seconds had finished by the time a wait sent after the click began, so its middle could be read
|
|
252
|
+
## only by slowing the game's clock. Armed before the click, the watch has every frame of it.
|
|
253
|
+
func _watch_property(node_path: String, property: String) -> Dictionary:
|
|
254
|
+
if node_path.is_empty():
|
|
255
|
+
return {"type": "error", "message": "Node path required"}
|
|
256
|
+
var standing: Dictionary = GdhValues.node_at(_host.get_tree().root, node_path)
|
|
257
|
+
if standing.has("message"):
|
|
258
|
+
return standing
|
|
259
|
+
var node: Node = standing["node"]
|
|
260
|
+
var watched: Dictionary = _watched(node, node_path, property)
|
|
261
|
+
if watched.has("message"):
|
|
262
|
+
return watched
|
|
263
|
+
var trace: GdhTrace = GdhTrace.new(node, node_path, property)
|
|
264
|
+
trace.see(0, 0, 0.0, watched["value"], _values)
|
|
265
|
+
while _traces.size() >= MOST_WATCHES:
|
|
266
|
+
var _gone: bool = _traces.erase(_traces.keys().min())
|
|
267
|
+
var id: int = _next_watch
|
|
268
|
+
_next_watch += 1
|
|
269
|
+
_traces[id] = trace
|
|
270
|
+
# Left to run across frames while the answer goes now. Deferred calls run once the frame's
|
|
271
|
+
# _process is done and before its tweens step, so the recording starts in the frame it was armed in.
|
|
272
|
+
_record.call_deferred(trace)
|
|
273
|
+
return {
|
|
274
|
+
"type": "watching",
|
|
275
|
+
"watch": id,
|
|
276
|
+
"path": node_path,
|
|
277
|
+
"property": property,
|
|
278
|
+
"value": _values.serialize(watched["value"]),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
## Reads the watched property at the start of every frame until the watch is collected, the node
|
|
283
|
+
## goes or the ceiling a wait has is reached.
|
|
284
|
+
##
|
|
285
|
+
## Read where a frame begins, which is after the last one's tweens and _process ran: the game time
|
|
286
|
+
## beside each reading is the time those had been given, which counts the frame the watch was armed
|
|
287
|
+
## in, since commands run before that frame's tweens step.
|
|
288
|
+
func _record(trace: GdhTrace) -> void:
|
|
289
|
+
var started: int = Time.get_ticks_msec()
|
|
290
|
+
var given: float = _host.get_process_delta_time()
|
|
291
|
+
var game_time: float = 0.0
|
|
292
|
+
var frame: int = 0
|
|
293
|
+
while not trace.collected:
|
|
294
|
+
await _host.get_tree().process_frame
|
|
295
|
+
if trace.collected:
|
|
296
|
+
return
|
|
297
|
+
frame += 1
|
|
298
|
+
game_time += given
|
|
299
|
+
given = _host.get_process_delta_time()
|
|
300
|
+
var elapsed: int = Time.get_ticks_msec() - started
|
|
301
|
+
var held: Object = trace.node.get_ref()
|
|
302
|
+
var node: Node = held as Node
|
|
303
|
+
if node == null:
|
|
304
|
+
trace.ended = "%s was freed %d frames after the watch was armed" % [trace.node_path, frame]
|
|
305
|
+
return
|
|
306
|
+
var watched: Dictionary = _watched(node, trace.node_path, trace.property)
|
|
307
|
+
if watched.has("message"):
|
|
308
|
+
trace.ended = "%d frames after the watch was armed: %s" % [frame, watched["message"]]
|
|
309
|
+
return
|
|
310
|
+
trace.see(frame, elapsed, game_time, watched["value"], _values)
|
|
311
|
+
trace.frames = frame
|
|
312
|
+
trace.elapsed_ms = elapsed
|
|
313
|
+
if elapsed >= CEILING_MSEC:
|
|
314
|
+
trace.ended = (
|
|
315
|
+
"recording stopped %d ms after the watch was armed, the longest a wait lasts" % elapsed
|
|
316
|
+
)
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
## Notes [param command] in every property watch still recording, beside the last reading taken
|
|
321
|
+
## before it ran, so a recording shows where the call that started a change landed. A call two
|
|
322
|
+
## round trips apart from another lands dozens of frames later, which only this tells apart from
|
|
323
|
+
## the game's own delay.
|
|
324
|
+
func saw_call(command: String, params: Dictionary) -> void:
|
|
325
|
+
var named: String = CALLS_NAMED.get(command, "")
|
|
326
|
+
if named.is_empty():
|
|
327
|
+
return
|
|
328
|
+
for trace: GdhTrace in _traces.values():
|
|
329
|
+
if trace.collected or not trace.ended.is_empty():
|
|
330
|
+
continue
|
|
331
|
+
if trace.calls.size() >= MOST_CHANGES:
|
|
332
|
+
trace.calls_dropped += 1
|
|
333
|
+
continue
|
|
334
|
+
var noted: Dictionary = {"frame": trace.frames, "call": named}
|
|
335
|
+
for field: String in CALL_FIELDS:
|
|
336
|
+
if params.has(field):
|
|
337
|
+
noted[field] = params[field]
|
|
338
|
+
trace.calls.append(noted)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
## What a property watch recorded, each change with the frame, the time and the game time it was
|
|
342
|
+
## read at. The watch goes with the answer.
|
|
343
|
+
func recorded(params: Dictionary) -> Dictionary:
|
|
344
|
+
var id: int = GdhRead.as_int(params.get("watch", 0), 0)
|
|
345
|
+
if _watches.has(id):
|
|
346
|
+
return {
|
|
347
|
+
"type": "error",
|
|
348
|
+
"message":
|
|
349
|
+
(
|
|
350
|
+
"Watch %d records the signal %s of %s, and is collected with runtime_wait signal"
|
|
351
|
+
% [id, _watches[id].signal_name, _watches[id].node_path]
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
if not _traces.has(id):
|
|
355
|
+
return {
|
|
356
|
+
"type": "error",
|
|
357
|
+
"message":
|
|
358
|
+
(
|
|
359
|
+
(
|
|
360
|
+
"No property watch %d is armed in this game: it was collected already, %d armed after"
|
|
361
|
+
+ " it let it go, or the game started again since"
|
|
362
|
+
)
|
|
363
|
+
% [id, MOST_WATCHES]
|
|
364
|
+
),
|
|
365
|
+
}
|
|
366
|
+
var trace: GdhTrace = _traces[id]
|
|
367
|
+
trace.collected = true
|
|
368
|
+
var _gone: bool = _traces.erase(id)
|
|
369
|
+
var answer: Dictionary = {
|
|
370
|
+
"type": "recorded",
|
|
371
|
+
"watch": id,
|
|
372
|
+
"path": trace.node_path,
|
|
373
|
+
"property": trace.property,
|
|
374
|
+
"changes": trace.changes,
|
|
375
|
+
"calls": trace.calls,
|
|
376
|
+
"frames": trace.frames,
|
|
377
|
+
"elapsed_ms": trace.elapsed_ms,
|
|
378
|
+
}
|
|
379
|
+
if trace.calls_dropped > 0:
|
|
380
|
+
answer["calls_dropped"] = trace.calls_dropped
|
|
381
|
+
if trace.dropped > 0:
|
|
382
|
+
answer["dropped"] = (
|
|
383
|
+
(
|
|
384
|
+
"%d changes are left out: past %d, each new one took the last place, so the list ends on"
|
|
385
|
+
+ " the latest"
|
|
386
|
+
)
|
|
387
|
+
% [trace.dropped, MOST_CHANGES]
|
|
388
|
+
)
|
|
389
|
+
if not trace.ended.is_empty():
|
|
390
|
+
answer["ended"] = trace.ended
|
|
391
|
+
return answer
|
|
392
|
+
|
|
393
|
+
|
|
197
394
|
## Waits until a property reads as the value given, or until something under the path says what
|
|
198
395
|
## was given, or the time runs out, and answers with what it read last either way.
|
|
199
396
|
func wait_until(params: Dictionary) -> Dictionary:
|
|
@@ -305,7 +502,7 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
305
502
|
return watched
|
|
306
503
|
current = watched["value"]
|
|
307
504
|
|
|
308
|
-
|
|
505
|
+
var answer: Dictionary = {
|
|
309
506
|
"type": "condition",
|
|
310
507
|
"path": node_path,
|
|
311
508
|
"property": property,
|
|
@@ -315,6 +512,16 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
315
512
|
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
316
513
|
"frames": frames,
|
|
317
514
|
}
|
|
515
|
+
# Said rather than refused, since a game may store a number no value has, as set says it stores
|
|
516
|
+
# one: a wait for 7 on an enum of three ran its whole timeout and answered only that it was not met.
|
|
517
|
+
if watched["holder"] is Object:
|
|
518
|
+
var owner: Object = watched["holder"]
|
|
519
|
+
var shown: String = JSON.stringify(_values.serialize(wanted), "", false)
|
|
520
|
+
var declaration: Dictionary = watched["declaration"]
|
|
521
|
+
var outside: String = GdhDeclared.outside(owner, declaration, wanted, shown)
|
|
522
|
+
if not outside.is_empty():
|
|
523
|
+
answer["outside_declared"] = outside
|
|
524
|
+
return answer
|
|
318
525
|
|
|
319
526
|
|
|
320
527
|
## What a property wait is missing or was given wrongly, or "" when it can go ahead.
|
|
@@ -589,7 +796,7 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
|
|
|
589
796
|
looks += 1
|
|
590
797
|
looked_usec += Time.get_ticks_usec() - last
|
|
591
798
|
|
|
592
|
-
|
|
799
|
+
var answer: Dictionary = {
|
|
593
800
|
"type": "condition",
|
|
594
801
|
"path": node_path,
|
|
595
802
|
"says": said,
|
|
@@ -600,12 +807,24 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
|
|
|
600
807
|
"looks": looks,
|
|
601
808
|
"looking_ms": looked_usec / 1000.0,
|
|
602
809
|
}
|
|
810
|
+
# Words that are there and cannot be read look, from a wait that ran out, like words that never
|
|
811
|
+
# came.
|
|
812
|
+
if not found and not include_hidden and _anything_says(node_path, said, true):
|
|
813
|
+
answer["unseen_note"] = (
|
|
814
|
+
(
|
|
815
|
+
"Something under %s says it, but is hidden or drawn outside the viewport or a"
|
|
816
|
+
+ " ScrollContainer, where the player cannot read it; includeHidden true counts it there too"
|
|
817
|
+
)
|
|
818
|
+
% node_path
|
|
819
|
+
)
|
|
820
|
+
return answer
|
|
603
821
|
|
|
604
822
|
|
|
605
|
-
## Whether anything under [param node_path] says [param said], the node itself included, and
|
|
606
|
-
## unless hidden ones are wanted too. Matched as a find and a click match
|
|
607
|
-
## whole text when it has `*` or `?`: read as literal words, a glob a click
|
|
608
|
-
## waited on for the whole timeout with its label on screen, and answered as
|
|
823
|
+
## Whether anything under [param node_path] says [param said], the node itself included, and where
|
|
824
|
+
## the player can read it unless hidden ones are wanted too. Matched as a find and a click match
|
|
825
|
+
## `says`, a glob over the whole text when it has `*` or `?`: read as literal words, a glob a click
|
|
826
|
+
## had just pressed by was waited on for the whole timeout with its label on screen, and answered as
|
|
827
|
+
## words never arriving.
|
|
609
828
|
func _anything_says(node_path: String, said: String, include_hidden: bool) -> bool:
|
|
610
829
|
var root: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
611
830
|
if root == null:
|
|
@@ -613,7 +832,7 @@ func _anything_says(node_path: String, said: String, include_hidden: bool) -> bo
|
|
|
613
832
|
var pending: Array[Node] = [root]
|
|
614
833
|
while not pending.is_empty():
|
|
615
834
|
var node: Node = pending.pop_back()
|
|
616
|
-
if GdhSays.says(node, said) and (include_hidden or GdhQueries.
|
|
835
|
+
if GdhSays.says(node, said) and (include_hidden or GdhQueries.readable(node)):
|
|
617
836
|
return true
|
|
618
837
|
pending.append_array(node.get_children(true))
|
|
619
838
|
return false
|
|
@@ -637,6 +856,55 @@ class GdhSignalCatcher:
|
|
|
637
856
|
times += 1
|
|
638
857
|
|
|
639
858
|
|
|
859
|
+
## A property recorded from when it was armed until it is collected: the reading at arming, then
|
|
860
|
+
## each frame's that differs from the one before.
|
|
861
|
+
class GdhTrace:
|
|
862
|
+
extends RefCounted
|
|
863
|
+
var node: WeakRef
|
|
864
|
+
var node_path: String
|
|
865
|
+
var property: String
|
|
866
|
+
var changes: Array[Dictionary] = []
|
|
867
|
+
var dropped: int = 0
|
|
868
|
+
var calls: Array[Dictionary] = []
|
|
869
|
+
var calls_dropped: int = 0
|
|
870
|
+
var frames: int = 0
|
|
871
|
+
var elapsed_ms: int = 0
|
|
872
|
+
var ended: String = ""
|
|
873
|
+
var collected: bool = false
|
|
874
|
+
var _last: Variant = null
|
|
875
|
+
var _seen: bool = false
|
|
876
|
+
|
|
877
|
+
func _init(watched: Node, path: String, named: String) -> void:
|
|
878
|
+
node = weakref(watched)
|
|
879
|
+
node_path = path
|
|
880
|
+
property = named
|
|
881
|
+
|
|
882
|
+
func see(frame: int, elapsed: int, game_time: float, value: Variant, values: GdhValues) -> void:
|
|
883
|
+
if _seen and typeof(value) == typeof(_last) and value == _last:
|
|
884
|
+
return
|
|
885
|
+
_seen = true
|
|
886
|
+
# A copy of a list or a map, which the game may change in place: held as it is, the reading
|
|
887
|
+
# kept is the game's own, and it reads the same as every later one.
|
|
888
|
+
_last = value
|
|
889
|
+
if value is Array:
|
|
890
|
+
var list: Array = value
|
|
891
|
+
_last = list.duplicate(true)
|
|
892
|
+
elif value is Dictionary:
|
|
893
|
+
var map: Dictionary = value
|
|
894
|
+
_last = map.duplicate(true)
|
|
895
|
+
var change: Dictionary = {
|
|
896
|
+
"frame": frame,
|
|
897
|
+
"elapsed_ms": elapsed,
|
|
898
|
+
"game_time": snappedf(game_time, 0.0001),
|
|
899
|
+
"value": values.serialize(value),
|
|
900
|
+
}
|
|
901
|
+
if changes.size() < MOST_CHANGES:
|
|
902
|
+
changes.append(change)
|
|
903
|
+
else:
|
|
904
|
+
changes[changes.size() - 1] = change
|
|
905
|
+
dropped += 1
|
|
906
|
+
|
|
907
|
+
|
|
640
908
|
## A signal recorded from when it was armed until it is collected.
|
|
641
909
|
class GdhWatch:
|
|
642
910
|
extends RefCounted
|
|
@@ -55,6 +55,33 @@ static func lines_said_by(node: Node) -> Array[String]:
|
|
|
55
55
|
return lines
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
## The node [param node] is drawn as a copy of, or null: the nearest node above it that says
|
|
59
|
+
## anything, when it says the same words, followed up while that one is a copy in turn. A word drawn
|
|
60
|
+
## with an outline or a glow is often a label holding copies of itself, offset or tinted, and a
|
|
61
|
+
## player reads one sign where a find counted four.
|
|
62
|
+
static func copied_from(node: Node) -> Node:
|
|
63
|
+
var chain: Array[Node] = copied_through(node)
|
|
64
|
+
return null if chain.is_empty() else chain.back()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Every node above [param node] that it is drawn as a copy of, nearest first: see
|
|
68
|
+
## [method copied_from], which is the last of them.
|
|
69
|
+
static func copied_through(node: Node) -> Array[Node]:
|
|
70
|
+
var chain: Array[Node] = []
|
|
71
|
+
var words: String = said_by(node)
|
|
72
|
+
if words.is_empty():
|
|
73
|
+
return chain
|
|
74
|
+
var above: Node = node.get_parent()
|
|
75
|
+
while above != null:
|
|
76
|
+
var said: String = said_by(above)
|
|
77
|
+
if not said.is_empty():
|
|
78
|
+
if said != words:
|
|
79
|
+
break
|
|
80
|
+
chain.append(above)
|
|
81
|
+
above = above.get_parent()
|
|
82
|
+
return chain
|
|
83
|
+
|
|
84
|
+
|
|
58
85
|
## Each item [param node] draws as one of a list, as {"says", "item"}: a tab, a menu title, a list
|
|
59
86
|
## entry or a menu item by its index, and a tree row by its TreeItem. Empty for a node drawing none.
|
|
60
87
|
## Held apart rather than joined, so a click on a tab's title presses that tab: the bar's middle is
|
|
@@ -142,28 +142,29 @@ func serialize_value(value: Variant) -> Variant:
|
|
|
142
142
|
return value
|
|
143
143
|
var carried: Variant = call(serialiser, value)
|
|
144
144
|
# The fixed shapes carry their components as bare floats, so an infinite one in a Vector2 is
|
|
145
|
-
# tagged here; containers reach their members through this
|
|
146
|
-
|
|
145
|
+
# tagged here, and a 32-bit one shortened; containers reach their members through this already.
|
|
146
|
+
var type: int = typeof(value)
|
|
147
|
+
return carried if type in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried, GdhRead.holds_singles(type))
|
|
147
148
|
|
|
148
149
|
|
|
149
150
|
# [param carried] with every non-finite float in it tagged. JSON has no spelling for infinity or
|
|
150
151
|
# NaN: the engine writes 1e99999 and null, and the server, reading 1e99999 as Infinity, writes it
|
|
151
152
|
# back out as null, so a property holding INF was answered as holding nothing.
|
|
152
|
-
func _finite(carried: Variant) -> Variant:
|
|
153
|
+
func _finite(carried: Variant, single: bool = false) -> Variant:
|
|
153
154
|
if carried is float:
|
|
154
155
|
var number: float = carried
|
|
155
|
-
return _serialize_float(number)
|
|
156
|
+
return _serialize_float(GdhRead.single(number) if single else number)
|
|
156
157
|
if carried is Dictionary:
|
|
157
158
|
var fields: Dictionary = carried
|
|
158
159
|
var tagged: Dictionary = {}
|
|
159
160
|
for key: Variant in fields:
|
|
160
|
-
tagged[key] = _finite(fields[key])
|
|
161
|
+
tagged[key] = _finite(fields[key], single)
|
|
161
162
|
return tagged
|
|
162
163
|
if carried is Array:
|
|
163
164
|
var items: Array = carried
|
|
164
165
|
var tagged_items: Array = []
|
|
165
166
|
for item: Variant in items:
|
|
166
|
-
tagged_items.append(_finite(item))
|
|
167
|
+
tagged_items.append(_finite(item, single))
|
|
167
168
|
return tagged_items
|
|
168
169
|
return carried
|
|
169
170
|
|
|
@@ -189,7 +190,7 @@ func _serialize_floats(value: Variant) -> Variant:
|
|
|
189
190
|
for each: float in numbers:
|
|
190
191
|
items.append(_serialize_float(each))
|
|
191
192
|
return items
|
|
192
|
-
return value
|
|
193
|
+
return numbers if value is PackedFloat32Array else value
|
|
193
194
|
|
|
194
195
|
|
|
195
196
|
# Rebuilds a Godot value from the shape serialize_value gave it.
|
|
@@ -222,8 +223,7 @@ func deserialize_value(value: Variant) -> Variant:
|
|
|
222
223
|
# under the target project's warning levels, so one such project lost every headless call.
|
|
223
224
|
var tag: String = str(fields["_type"])
|
|
224
225
|
var builder: String = BUILDERS.get(tag, "")
|
|
225
|
-
|
|
226
|
-
if builder.is_empty() or not spare.any(func(left: Array) -> bool: return left.is_empty()):
|
|
226
|
+
if builder.is_empty() or not GdhShapes.buildable(fields, tag):
|
|
227
227
|
return fields
|
|
228
228
|
if tag == "float" and str(fields["value"]) not in ["inf", "-inf", "nan"]:
|
|
229
229
|
return fields
|
|
@@ -276,24 +276,7 @@ static func components_of(value: Variant) -> PackedStringArray:
|
|
|
276
276
|
TYPE_VECTOR4, TYPE_VECTOR4I, TYPE_QUATERNION:
|
|
277
277
|
return PackedStringArray(["x", "y", "z", "w"])
|
|
278
278
|
TYPE_COLOR:
|
|
279
|
-
return
|
|
280
|
-
[
|
|
281
|
-
"r",
|
|
282
|
-
"g",
|
|
283
|
-
"b",
|
|
284
|
-
"a",
|
|
285
|
-
"r8",
|
|
286
|
-
"g8",
|
|
287
|
-
"b8",
|
|
288
|
-
"a8",
|
|
289
|
-
"h",
|
|
290
|
-
"s",
|
|
291
|
-
"v",
|
|
292
|
-
"ok_hsl_h",
|
|
293
|
-
"ok_hsl_s",
|
|
294
|
-
"ok_hsl_l"
|
|
295
|
-
]
|
|
296
|
-
)
|
|
279
|
+
return "r g b a r8 g8 b8 a8 h s v ok_hsl_h ok_hsl_s ok_hsl_l".split(" ")
|
|
297
280
|
TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
|
|
298
281
|
return PackedStringArray(["position", "size", "end"])
|
|
299
282
|
TYPE_PLANE:
|
|
@@ -404,7 +387,8 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
404
387
|
return built
|
|
405
388
|
|
|
406
389
|
if rebuilt is Array and LIST_TYPES.has(type):
|
|
407
|
-
|
|
390
|
+
var items: Array = value if value is Array else rebuilt
|
|
391
|
+
return GdhShapes.packed_from(items, type, fitted)
|
|
408
392
|
|
|
409
393
|
# The inspector and every colour picker give a colour as its html code. Anything else stays the
|
|
410
394
|
# text it is, for the refusal to name.
|
|
@@ -704,7 +688,13 @@ func _describe_built_in(resource: Resource) -> Dictionary:
|
|
|
704
688
|
var script: Script = null
|
|
705
689
|
if attached is Script:
|
|
706
690
|
script = attached
|
|
707
|
-
|
|
691
|
+
# By the class its script declares, which a write takes as the _type and which says what it
|
|
692
|
+
# is: as Resource with the script beside it, one answer named a Shop's built-in item that way
|
|
693
|
+
# and the file item beside it as an Item.
|
|
694
|
+
if script.get_global_name().is_empty():
|
|
695
|
+
described["script"] = serialize_value(script)
|
|
696
|
+
else:
|
|
697
|
+
described["_type"] = str(script.get_global_name())
|
|
708
698
|
var fresh: Object = _fresh(script)
|
|
709
699
|
for property: Dictionary in resource.get_property_list():
|
|
710
700
|
var usage: int = property.get("usage", 0)
|
|
@@ -5,6 +5,22 @@ extends RefCounted
|
|
|
5
5
|
##
|
|
6
6
|
## This file is the one beside the operations, and `bun run sync:gd` copies it into each addon.
|
|
7
7
|
|
|
8
|
+
const GdhRead = preload("reading.gd")
|
|
9
|
+
|
|
10
|
+
## What each packed array holds, one value of which every element is fitted to.
|
|
11
|
+
const PACKED_ELEMENTS: Dictionary = {
|
|
12
|
+
TYPE_PACKED_BYTE_ARRAY: TYPE_INT,
|
|
13
|
+
TYPE_PACKED_INT32_ARRAY: TYPE_INT,
|
|
14
|
+
TYPE_PACKED_INT64_ARRAY: TYPE_INT,
|
|
15
|
+
TYPE_PACKED_FLOAT32_ARRAY: TYPE_FLOAT,
|
|
16
|
+
TYPE_PACKED_FLOAT64_ARRAY: TYPE_FLOAT,
|
|
17
|
+
TYPE_PACKED_STRING_ARRAY: TYPE_STRING,
|
|
18
|
+
TYPE_PACKED_VECTOR2_ARRAY: TYPE_VECTOR2,
|
|
19
|
+
TYPE_PACKED_VECTOR3_ARRAY: TYPE_VECTOR3,
|
|
20
|
+
TYPE_PACKED_VECTOR4_ARRAY: TYPE_VECTOR4,
|
|
21
|
+
TYPE_PACKED_COLOR_ARRAY: TYPE_COLOR,
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
# What each tag has to carry before it is built. A tag naming a type whose keys are not there is a
|
|
9
25
|
# tag somebody wrote by mistake or wrote for a different shape, and building one out of defaults
|
|
10
26
|
# answers with a Transform2D of zeros where the caller meant something else entirely. Unbuilt, it
|
|
@@ -33,6 +49,21 @@ const REQUIRED: Dictionary = {
|
|
|
33
49
|
"RID": [["id"]],
|
|
34
50
|
"Dictionary": [["entries"]],
|
|
35
51
|
}
|
|
52
|
+
## The fields each type reads as numbers. A builder reads a word there as 0, so {"x": 0, "y": "far"}
|
|
53
|
+
## was written as (0, 0) and answered as written.
|
|
54
|
+
const NUMBER_FIELDS: Dictionary = {
|
|
55
|
+
"Vector2": ["x", "y"],
|
|
56
|
+
"Vector2i": ["x", "y"],
|
|
57
|
+
"Vector3": ["x", "y", "z"],
|
|
58
|
+
"Vector3i": ["x", "y", "z"],
|
|
59
|
+
"Vector4": ["x", "y", "z", "w"],
|
|
60
|
+
"Vector4i": ["x", "y", "z", "w"],
|
|
61
|
+
"Rect2": ["x", "y", "width", "height"],
|
|
62
|
+
"Rect2i": ["x", "y", "width", "height"],
|
|
63
|
+
"Plane": ["d"],
|
|
64
|
+
"Quaternion": ["x", "y", "z", "w"],
|
|
65
|
+
"Color": ["r", "g", "b", "a"],
|
|
66
|
+
}
|
|
36
67
|
## The fields a builder reads when they are there and does without when not.
|
|
37
68
|
const OPTIONAL: Dictionary = {
|
|
38
69
|
"Color": ["a"],
|
|
@@ -77,6 +108,86 @@ static func spare(fields: Dictionary, tag: String) -> Array:
|
|
|
77
108
|
return lists
|
|
78
109
|
|
|
79
110
|
|
|
111
|
+
## Whether a [param tag] built from [param fields] reads every one of them, and each it reads as a
|
|
112
|
+
## number holds one.
|
|
113
|
+
static func buildable(fields: Dictionary, tag: String) -> bool:
|
|
114
|
+
return (
|
|
115
|
+
spare(fields, tag).any(func(left: Array) -> bool: return left.is_empty())
|
|
116
|
+
and not_a_number(fields, tag).is_empty()
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
## The first field of [param fields] that a [param tag] reads as a number and that holds no number,
|
|
121
|
+
## or "" when there is none. Digits in quotes are a number, as a write takes them for a float.
|
|
122
|
+
static func not_a_number(fields: Dictionary, tag: String) -> String:
|
|
123
|
+
for key: String in NUMBER_FIELDS.get(tag, []):
|
|
124
|
+
if fields.has(key) and not _is_number(fields[key]):
|
|
125
|
+
return key
|
|
126
|
+
return ""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
## Whether every item of [param items] is a number, for a list read by its places as a vector.
|
|
130
|
+
static func all_numbers(items: Array) -> bool:
|
|
131
|
+
return items.all(_is_number)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# A float JSON has no number for, infinity or NaN, crosses tagged as {"_type": "float", "value":
|
|
135
|
+
# "inf"}, and is a number.
|
|
136
|
+
static func _is_number(held: Variant) -> bool:
|
|
137
|
+
if held is int or held is float:
|
|
138
|
+
return true
|
|
139
|
+
if held is String:
|
|
140
|
+
return str(held).is_valid_float()
|
|
141
|
+
if held is Dictionary:
|
|
142
|
+
var tagged: Dictionary = held
|
|
143
|
+
return str(tagged.get("_type", "")) in ["float", "int"] and tagged.has("value")
|
|
144
|
+
return false
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
## Whether [param value] holds a number with a fractional part where an integer is wanted, in a
|
|
148
|
+
## list or a dictionary as much as bare. The conversion would cut 2.7 to 2 without a word, and a
|
|
149
|
+
## dictionary key arrives as the text "2.7", which reads as the same number.
|
|
150
|
+
static func fraction(value: Variant, type: int, elements: Array = []) -> bool:
|
|
151
|
+
if type == TYPE_INT:
|
|
152
|
+
var number: Variant = value
|
|
153
|
+
if typeof(value) == TYPE_STRING:
|
|
154
|
+
number = GdhRead.json_or_null(str(value))
|
|
155
|
+
if typeof(number) != TYPE_FLOAT:
|
|
156
|
+
return false
|
|
157
|
+
var whole: float = number
|
|
158
|
+
return is_finite(whole) and whole != floorf(whole)
|
|
159
|
+
var element: int = GdhRead.as_int(PACKED_ELEMENTS.get(type, TYPE_NIL), TYPE_NIL)
|
|
160
|
+
if type == TYPE_ARRAY and not elements.is_empty():
|
|
161
|
+
element = GdhRead.as_int(elements[0], TYPE_NIL)
|
|
162
|
+
if value is Array and element != TYPE_NIL:
|
|
163
|
+
var items: Array = value
|
|
164
|
+
return items.any(func(item: Variant) -> bool: return fraction(item, element))
|
|
165
|
+
if type == TYPE_DICTIONARY and elements.size() == 2 and value is Dictionary:
|
|
166
|
+
var entries: Dictionary = value
|
|
167
|
+
var key_type: int = GdhRead.as_int(elements[0], TYPE_NIL)
|
|
168
|
+
var value_type: int = GdhRead.as_int(elements[1], TYPE_NIL)
|
|
169
|
+
for key: Variant in entries:
|
|
170
|
+
if fraction(key, key_type) or fraction(entries[key], value_type):
|
|
171
|
+
return true
|
|
172
|
+
return false
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## The packed array [param type] built from [param items], each fitted by [param fit] to what the
|
|
176
|
+
## array holds, or [param items] as they are when one cannot become that, for the caller to refuse.
|
|
177
|
+
## Element by element, so a point given as its fields is read as one: converted whole, a list of
|
|
178
|
+
## {"x": 3, "y": 4} became zero vectors, and the call ran on them.
|
|
179
|
+
static func packed_from(items: Array, type: int, fit: Callable) -> Variant:
|
|
180
|
+
var element: int = PACKED_ELEMENTS[type]
|
|
181
|
+
var numbers: Array[int] = [TYPE_INT, TYPE_FLOAT]
|
|
182
|
+
var built: Array = []
|
|
183
|
+
for item: Variant in items:
|
|
184
|
+
var one: Variant = fit.call(item, element)
|
|
185
|
+
if typeof(one) != element and not (numbers.has(typeof(one)) and numbers.has(element)):
|
|
186
|
+
return items
|
|
187
|
+
built.append(one)
|
|
188
|
+
return type_convert(built, type)
|
|
189
|
+
|
|
190
|
+
|
|
80
191
|
## The sentences a refusal of [param value] where a [param type] goes ends with: which of its keys
|
|
81
192
|
## kept it from being read as one, and how one is written. Empty for a type not built from fields.
|
|
82
193
|
##
|
|
@@ -92,7 +203,23 @@ static func remedy(value: Variant, type: int) -> String:
|
|
|
92
203
|
var shape: String = written_as(type)
|
|
93
204
|
if shape.is_empty():
|
|
94
205
|
return ""
|
|
95
|
-
return
|
|
206
|
+
return (
|
|
207
|
+
_unread(value, type) + _no_number(value, type) + " A %s is sent as %s." % [type_string(type), shape]
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
static func _no_number(value: Variant, type: int) -> String:
|
|
212
|
+
var tag: String = type_string(type)
|
|
213
|
+
if value is Dictionary:
|
|
214
|
+
var fields: Dictionary = value
|
|
215
|
+
var key: String = not_a_number(fields, tag)
|
|
216
|
+
if not key.is_empty():
|
|
217
|
+
return " Its %s is %s, which is no number." % [key, JSON.stringify(fields[key])]
|
|
218
|
+
if value is Array and NUMBER_FIELDS.has(tag):
|
|
219
|
+
var items: Array = value
|
|
220
|
+
if not all_numbers(items):
|
|
221
|
+
return " Read by its places, each item is a number."
|
|
222
|
+
return ""
|
|
96
223
|
|
|
97
224
|
|
|
98
225
|
static func _unread(value: Variant, type: int) -> String:
|