gdharness 1.2.35 → 1.2.36

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.
@@ -26,8 +26,13 @@ const LOOKS_APART: int = 9
26
26
  ## two looks and a wait for 32 never sees it.
27
27
  const COMPARES: Array[String] = ["==", "!=", ">=", "<=", ">", "<"]
28
28
 
29
+ ## The most signal watches kept at once; arming one past this lets the oldest go.
30
+ const MOST_WATCHES: int = 16
31
+
29
32
  var _host: Node
30
33
  var _values: Values
34
+ var _watches: Dictionary[int, Watch] = {}
35
+ var _next_watch: int = 1
31
36
 
32
37
 
33
38
  func _init(host: Node, values: Values) -> void:
@@ -56,13 +61,16 @@ func wait_frames(params: Dictionary) -> Dictionary:
56
61
  return {"type": "waited", "frames": frames, "elapsed_ms": Time.get_ticks_msec() - started}
57
62
 
58
63
 
59
- ## Waits for a signal to fire, and answers with what it carried, or that the time ran out.
64
+ ## Waits for a signal to fire, and answers with what it carried, or that the time ran out. Given a
65
+ ## watch, collects that instead: see [method watch_signal].
60
66
  func wait_signal(params: Dictionary) -> Dictionary:
61
67
  var node_path: String = str(params.get("path", ""))
62
68
  var signal_name: String = str(params.get("signal", ""))
63
69
  var timeout_ms: int = Read.as_int(params.get("timeout_ms", 5000), 5000)
64
70
  if timeout_ms < 1 or timeout_ms > CEILING_MSEC:
65
71
  return _out_of_range("timeout_ms", timeout_ms, 1, CEILING_MSEC)
72
+ if params.has("watch"):
73
+ return await _collect(Read.as_int(params["watch"], 0), timeout_ms)
66
74
  if node_path.is_empty() or signal_name.is_empty():
67
75
  return {"type": "error", "message": "Node path and signal name required"}
68
76
 
@@ -84,7 +92,7 @@ func wait_signal(params: Dictionary) -> Dictionary:
84
92
  if not catcher.fired and is_instance_valid(node) and node.is_connected(signal_name, callable):
85
93
  node.disconnect(signal_name, callable)
86
94
 
87
- return {
95
+ var answer: Dictionary = {
88
96
  "type": "signal",
89
97
  "path": node_path,
90
98
  "signal": signal_name,
@@ -92,6 +100,90 @@ func wait_signal(params: Dictionary) -> Dictionary:
92
100
  "args": _values.serialize(catcher.args),
93
101
  "elapsed_ms": Time.get_ticks_msec() - started,
94
102
  }
103
+ if not catcher.fired:
104
+ answer["note"] = (
105
+ "If a call meant to set the signal off was sent beside this wait, it may have run only after"
106
+ + " the wait answered, as a client can run its calls in turn: arm runtime_wait watch first,"
107
+ + " make the call, then wait with signal and that watch"
108
+ )
109
+ return answer
110
+
111
+
112
+ ## Starts recording a signal and answers at once with the watch to collect it by.
113
+ ##
114
+ ## A wait sent beside the call meant to set its signal off does not see it when the client runs the
115
+ ## two in turn: Claude Code ran a key press only after the signal wait beside it had answered, so the
116
+ ## wait ran out and the press landed after it. A watch is armed before the call and collected after.
117
+ func watch_signal(params: Dictionary) -> Dictionary:
118
+ var node_path: String = str(params.get("path", ""))
119
+ var signal_name: String = str(params.get("signal", ""))
120
+ if node_path.is_empty() or signal_name.is_empty():
121
+ return {"type": "error", "message": "Node path and signal name required"}
122
+ var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
123
+ if standing.has("message"):
124
+ return standing
125
+ var node: Node = standing["node"]
126
+ if not node.has_signal(signal_name):
127
+ return {"type": "error", "message": "%s has no signal %s" % [node_path, signal_name]}
128
+ var watch: Watch = Watch.new(node, node_path, signal_name)
129
+ var listening: Error = node.connect(signal_name, watch.catcher._on_fired)
130
+ if listening != OK:
131
+ return {"type": "error", "message": "Could not listen to signal: " + signal_name}
132
+ while _watches.size() >= MOST_WATCHES:
133
+ var oldest: int = _watches.keys().min()
134
+ _let_go(oldest)
135
+ var id: int = _next_watch
136
+ _next_watch += 1
137
+ _watches[id] = watch
138
+ return {"type": "watching", "watch": id, "path": node_path, "signal": signal_name}
139
+
140
+
141
+ ## Answers at once with the first emission recorded since watch [param id] was armed, otherwise
142
+ ## waits for the next until the time runs out. The watch goes either way.
143
+ func _collect(id: int, timeout_ms: int) -> Dictionary:
144
+ if not _watches.has(id):
145
+ return {
146
+ "type": "error",
147
+ "message":
148
+ (
149
+ (
150
+ "No watch %d is armed in this game: it was collected already, %d armed after it let it"
151
+ + " go, or the game started again since"
152
+ )
153
+ % [id, MOST_WATCHES]
154
+ ),
155
+ }
156
+ var watch: Watch = _watches[id]
157
+ var started: int = Time.get_ticks_msec()
158
+ var before: bool = watch.catcher.fired
159
+ while not watch.catcher.fired and Time.get_ticks_msec() - started < timeout_ms:
160
+ await _host.get_tree().process_frame
161
+ _let_go(id)
162
+ var answer: Dictionary = {
163
+ "type": "signal",
164
+ "path": watch.node_path,
165
+ "signal": watch.signal_name,
166
+ "watch": id,
167
+ "fired": watch.catcher.fired,
168
+ "times": watch.catcher.times,
169
+ "args": _values.serialize(watch.catcher.args),
170
+ "elapsed_ms": Time.get_ticks_msec() - started,
171
+ }
172
+ if before:
173
+ answer["fired_before_wait"] = true
174
+ return answer
175
+
176
+
177
+ func _let_go(id: int) -> void:
178
+ var watch: Watch = _watches.get(id)
179
+ var _gone: bool = _watches.erase(id)
180
+ if watch == null:
181
+ return
182
+ var held: Object = watch.node.get_ref()
183
+ var node: Node = held as Node
184
+ var callable: Callable = watch.catcher._on_fired
185
+ if node != null and node.is_connected(watch.signal_name, callable):
186
+ node.disconnect(watch.signal_name, callable)
95
187
 
96
188
 
97
189
  ## Waits until a property reads as the value given, or until something under the path says what
@@ -492,8 +584,26 @@ func _anything_says(node_path: String, said: String, include_hidden: bool) -> bo
492
584
  class SignalCatcher:
493
585
  extends RefCounted
494
586
  var fired: bool = false
587
+ ## How many times it fired; [member args] holds what the first carried.
588
+ var times: int = 0
495
589
  var args: Array = []
496
590
 
497
591
  func _on_fired(...carried: Array) -> void:
592
+ if not fired:
593
+ args = carried
498
594
  fired = true
499
- args = carried
595
+ times += 1
596
+
597
+
598
+ ## A signal recorded from when it was armed until it is collected.
599
+ class Watch:
600
+ extends RefCounted
601
+ var node: WeakRef
602
+ var node_path: String
603
+ var signal_name: String
604
+ var catcher: SignalCatcher = SignalCatcher.new()
605
+
606
+ func _init(watched: Node, path: String, named: String) -> void:
607
+ node = weakref(watched)
608
+ node_path = path
609
+ signal_name = named
@@ -21,6 +21,17 @@ const CATEGORY_BASES: Dictionary = {
21
21
  "visual": ["VisualInstance3D"],
22
22
  "animation": ["AnimationMixer"],
23
23
  }
24
+ ## The engine's MethodFlags, each by the name an answer gives it.
25
+ const METHOD_FLAG_NAMES: Dictionary[int, String] = {
26
+ METHOD_FLAG_NORMAL: "normal",
27
+ METHOD_FLAG_EDITOR: "editor",
28
+ METHOD_FLAG_CONST: "const",
29
+ METHOD_FLAG_VIRTUAL: "virtual",
30
+ METHOD_FLAG_VARARG: "vararg",
31
+ METHOD_FLAG_STATIC: "static",
32
+ METHOD_FLAG_OBJECT_CORE: "object_core",
33
+ METHOD_FLAG_VIRTUAL_REQUIRED: "virtual_required",
34
+ }
24
35
 
25
36
  var _log: Log
26
37
  var _values: Serialisation = Serialisation.new()
@@ -333,6 +344,7 @@ func _methods_of(class_name_str: String, include_inherited: bool) -> Array[Dicti
333
344
  "class_name": return_info.get("class_name", "")
334
345
  },
335
346
  "flags": m.get("flags", 0),
347
+ "flag_names": _flag_names(Read.as_int(m.get("flags", 0))),
336
348
  # As values, tagged the way every other answer tags one: JSON writes a Vector2i it is
337
349
  # handed as its text, so TileMapLayer.set_cell's default atlas coords read "(-1, -1)".
338
350
  "default_args": _values.serialize_value(m.get("default_args", []))
@@ -343,6 +355,16 @@ func _methods_of(class_name_str: String, include_inherited: bool) -> Array[Dicti
343
355
  return methods
344
356
 
345
357
 
358
+ # The MethodFlags set in [param flags] by name, beside the number the engine reports, which reads as
359
+ # nothing without its enum: 1 is normal, and a const method is 5.
360
+ static func _flag_names(flags: int) -> Array[String]:
361
+ var named: Array[String] = []
362
+ for flag: int in METHOD_FLAG_NAMES:
363
+ if flags & flag:
364
+ named.append(METHOD_FLAG_NAMES[flag])
365
+ return named
366
+
367
+
346
368
  # What [param info]'s type is called: its class for an object of one, and otherwise the engine's own
347
369
  # name for the type, beside the number the engine reports, which reads as nothing without its enum.
348
370
  # Nil is a value that can be anything when the usage says so, and [param nil_name] otherwise: void
@@ -23,6 +23,11 @@ const REFERENCE_PATTERNS: Array[String] = [
23
23
 
24
24
  var _log: Log
25
25
  var _files: FileWalk = FileWalk.new()
26
+ ## The project's class names and the scripts declaring them, read the first time a script is walked
27
+ ## and kept for the call: walking every resource starts a walk per root, and each reading every
28
+ ## script again made the whole-project answer quadratic in the number of scripts.
29
+ var _classes_declared: Dictionary = {}
30
+ var _classes_read: bool = false
26
31
 
27
32
 
28
33
  # Everything one dependency walk carries: the settings it was started with, and the state it
@@ -91,12 +96,14 @@ func get_dependencies(params: Dictionary) -> Dictionary:
91
96
 
92
97
  total_resources = all_resources.size()
93
98
 
94
- var dep_count: int = 0
99
+ var references: int = 0
100
+ var distinct: Dictionary = {}
95
101
  for key: String in dependencies:
96
102
  # Through a typed local: what comes out of a Dictionary is Variant, and a project that
97
103
  # errors on handing one to a typed parameter will not compile this script at all.
98
104
  var walked: Array[Dictionary] = dependencies[key]
99
- dep_count += _count_recursive(walked)
105
+ references += _count_recursive(walked)
106
+ _gather_paths(walked, key, distinct)
100
107
 
101
108
  return {
102
109
  "dependencies": dependencies,
@@ -104,7 +111,10 @@ func get_dependencies(params: Dictionary) -> Dictionary:
104
111
  "summary":
105
112
  {
106
113
  "total_resources": total_resources,
107
- "total_dependencies": dep_count,
114
+ # The files depended on, each once. The tree names a file again under everything that
115
+ # loads it, and counting its entries answered 11 for a scene with six dependencies.
116
+ "total_dependencies": distinct.size(),
117
+ "total_references": references,
108
118
  "circular_count": circular_references.size()
109
119
  }
110
120
  }
@@ -332,9 +342,24 @@ func _regex_escaped(text: String) -> String:
332
342
  return escaped
333
343
 
334
344
 
345
+ # [param root] is left out: a cycle leads back to it, and it is not one of its own dependencies.
346
+ func _gather_paths(deps: Array[Dictionary], root: String, into: Dictionary) -> void:
347
+ for dep: Dictionary in deps:
348
+ var path: String = str(dep.get("path", ""))
349
+ if path != root:
350
+ into[path] = true
351
+ if dep.has("dependencies"):
352
+ var deeper: Array[Dictionary] = dep["dependencies"]
353
+ _gather_paths(deeper, root, into)
354
+
355
+
356
+ # A circular entry is not counted: it marks that the entry holding it was reached again, which that
357
+ # entry already counts as one reference.
335
358
  func _count_recursive(deps: Array[Dictionary]) -> int:
336
- var count: int = deps.size()
359
+ var count: int = 0
337
360
  for dep: Dictionary in deps:
361
+ if not dep.get("circular", false):
362
+ count += 1
338
363
  if dep.has("dependencies"):
339
364
  var deeper: Array[Dictionary] = dep["dependencies"]
340
365
  count += _count_recursive(deeper)
@@ -390,29 +415,74 @@ func _analyze_resource(path: String, current_depth: int, walk: DependencyWalk) -
390
415
  if not walk.include_built_in and dep_path.begins_with("res://."):
391
416
  continue
392
417
 
393
- if dep_path == path:
394
- continue
395
-
396
- var dep_info: Dictionary = {"path": dep_path, "exists": FileAccess.file_exists(dep_path)}
397
-
398
- if dep_info["exists"] and current_depth + 1 < walk.max_depth:
399
- var sub_deps: Array[Dictionary] = _analyze_resource(dep_path, current_depth + 1, walk)
400
- if sub_deps.size() > 0:
401
- dep_info["dependencies"] = sub_deps
418
+ _depend(deps, path, dep_path, "", current_depth, walk)
402
419
 
403
- var already_added: bool = false
404
- for existing: Dictionary in deps:
405
- if existing.get("path", "") == dep_path:
406
- already_added = true
407
- break
408
- if not already_added:
409
- deps.append(dep_info)
420
+ # A script uses another by its class_name as surely as by a preload, and only the reverse walk
421
+ # looked for that: asked what a script depends on, the answer left out the script it typed a
422
+ # variable with, while asking what used that script named this one.
423
+ if path.get_extension() == "gd":
424
+ var classes: Dictionary = classes_declared()
425
+ for used: String in classes_used_in(content, classes):
426
+ _depend(deps, path, str(classes[used]), used, current_depth, walk)
410
427
 
411
428
  walk.path_stack.pop_back()
412
429
  walk.visited[path] = {"deps": deps, "remaining": remaining}
413
430
  return deps
414
431
 
415
432
 
433
+ # Adds [param dep_path] to [param deps], what [param path] depends on, walked as far as the depth left
434
+ # allows, once however many times it is named. [param by_class] is the class name it was named by,
435
+ # or empty for a path.
436
+ func _depend(
437
+ deps: Array[Dictionary],
438
+ path: String,
439
+ dep_path: String,
440
+ by_class: String,
441
+ current_depth: int,
442
+ walk: DependencyWalk
443
+ ) -> void:
444
+ if dep_path == path:
445
+ return
446
+ for existing: Dictionary in deps:
447
+ if existing.get("path", "") == dep_path:
448
+ return
449
+ var dep_info: Dictionary = {"path": dep_path, "exists": FileAccess.file_exists(dep_path)}
450
+ if not by_class.is_empty():
451
+ dep_info["by_class"] = by_class
452
+ if dep_info["exists"] and current_depth + 1 < walk.max_depth:
453
+ var sub_deps: Array[Dictionary] = _analyze_resource(dep_path, current_depth + 1, walk)
454
+ if sub_deps.size() > 0:
455
+ dep_info["dependencies"] = sub_deps
456
+ deps.append(dep_info)
457
+
458
+
459
+ ## Every class_name the project's scripts declare, with the script declaring it.
460
+ func classes_declared() -> Dictionary:
461
+ if _classes_read:
462
+ return _classes_declared
463
+ _classes_read = true
464
+ for script: String in _files.find_files("res://", ".gd"):
465
+ var declared: String = _declared_class_name(script)
466
+ if not declared.is_empty():
467
+ _classes_declared[declared] = script
468
+ return _classes_declared
469
+
470
+
471
+ ## The names in [param classes] that the code in [param content] uses, in the order first used: words
472
+ ## outside strings and comments, so a class named in prose or in a message is not a use of it.
473
+ static func classes_used_in(content: String, classes: Dictionary) -> Array[String]:
474
+ var used: Array[String] = []
475
+ if classes.is_empty():
476
+ return used
477
+ var word: RegEx = Patterns.compiled("[A-Za-z_][A-Za-z0-9_]*")
478
+ for line: String in content.split("\n"):
479
+ for m: RegExMatch in word.search_all(_code_of(line)):
480
+ var name: String = m.get_string()
481
+ if classes.has(name) and not name in used:
482
+ used.append(name)
483
+ return used
484
+
485
+
416
486
  # [param written] as the res:// path the engine would load from [param from]: a uid:// by what the
417
487
  # project's uid cache says it names, and a relative path against the referring file's directory.
418
488
  # Both were passed over as not being res:// paths, so a script's `preload("sibling.gd")` was no
@@ -1,10 +1,13 @@
1
1
  extends RefCounted
2
2
 
3
+ const Dependencies = preload("dependencies.gd")
3
4
  const Patterns = preload("patterns.gd")
4
5
  const Read = preload("reading.gd")
5
6
  const Log = preload("logger.gd")
6
7
 
7
8
  var _log: Log
9
+ ## Kept for the call, so the project's class names are read once however many bases are walked.
10
+ var _dependencies: Dependencies = null
8
11
 
9
12
 
10
13
  func _init(p_log: Log) -> void:
@@ -136,6 +139,21 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
136
139
  # its first function would otherwise be judged against the default.
137
140
  _mark_virtuals(functions, extends_name, full_script_path)
138
141
 
142
+ # A script typed by its class_name is depended on as surely as one preloaded, and the answer
143
+ # listed only the preloads: a `var stats: Stats` left stats.gd out, while project_dependencies
144
+ # named it. The same reading as that tool's, so the two agree.
145
+ var classes_used: Array[Dictionary] = []
146
+ if _dependencies == null:
147
+ _dependencies = Dependencies.new(_log)
148
+ var declared_by: Dictionary = _dependencies.classes_declared()
149
+ for used: String in Dependencies.classes_used_in(content, declared_by):
150
+ var declaring: String = str(declared_by[used])
151
+ if declaring == full_script_path:
152
+ continue
153
+ classes_used.append({"name": used, "path": declaring})
154
+ if not declaring in dependencies:
155
+ dependencies.append(declaring)
156
+
139
157
  var answer: Dictionary = {
140
158
  "path": script_path,
141
159
  "full_path": full_script_path,
@@ -148,6 +166,7 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
148
166
  "enums": enums,
149
167
  "inner_classes": inner_classes,
150
168
  "dependencies": dependencies,
169
+ "classes_used": classes_used,
151
170
  "line_count": lines.size()
152
171
  }
153
172
  if Read.as_bool(params.get("include_inherited", false)):
@@ -32,6 +32,7 @@ const ProjectDiagnostics = preload("project_diagnostics.gd")
32
32
  const Read = preload("reading.gd")
33
33
  const RenameChecks = preload("rename_checks.gd")
34
34
  const ResourceFiles = preload("resource_files.gd")
35
+ const ScriptCheck = preload("script_check.gd")
35
36
 
36
37
  ## Printed once the answer is written, after which the engine runs the project's autoloads. Kept in
37
38
  ## step with `ANSWERED` in src/headless.ts.
@@ -218,7 +219,7 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
218
219
  "names_taken":
219
220
  payload = RenameChecks.new(_log).names_taken(params)
220
221
  "check_scripts":
221
- payload = RenameChecks.new(_log).check_scripts(params)
222
+ payload = ScriptCheck.new(_log).check_scripts(params)
222
223
 
223
224
  # Plugins and input
224
225
  "list_plugins":
@@ -1,6 +1,5 @@
1
1
  extends RefCounted
2
2
 
3
- const GdscriptAuthoring = preload("gdscript_authoring.gd")
4
3
  const Log = preload("logger.gd")
5
4
 
6
5
  var _log: Log
@@ -63,21 +62,3 @@ static func _named(entries: Array[Dictionary], name: String) -> bool:
63
62
  if str(entry.get("name", "")) == name:
64
63
  return true
65
64
  return false
66
-
67
-
68
- # Whether each script compiles in a fresh engine, which reads the class cache as it starts and so
69
- # resolves every class the cache lists. The engine's reasons go to stderr and come back with the
70
- # answer. It names the scripts it checked rather than counting them, since a caller told one was
71
- # checked could not say which.
72
- func check_scripts(params: Dictionary) -> Dictionary:
73
- var paths: Array = params.get("paths", [])
74
- var checked: Array[String] = []
75
- var failed: Array[String] = []
76
- for value: Variant in paths:
77
- var path: String = str(value)
78
- checked.append(path)
79
- var script: Script = ResourceLoader.load(path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
80
- if not GdscriptAuthoring._parses(script):
81
- _log.error("Does not compile: " + path)
82
- failed.append(path)
83
- return {"checked": checked, "failed": failed}
@@ -0,0 +1,98 @@
1
+ extends RefCounted
2
+
3
+ ## Compiles scripts the way the engine does when it loads them, and answers what it raised.
4
+ ##
5
+ ## The engine reports a script that does not compile as errors raised from GDScript::reload, each
6
+ ## naming the file and line, and they reach a Logger as well as stderr. Measured on 4.7.2: a script
7
+ ## whose dependency does not compile reports the dependency's errors at the dependency's path and one
8
+ ## of its own at line 0, "Compile Error: Failed to compile depended scripts", and is still handed
9
+ ## back as a script that can_instantiate() calls instantiable. Warnings are not among them: the engine
10
+ ## sends those only to an editor or a debugger.
11
+ ##
12
+ ## A script that failed stays failed in the engine's script cache, which no ResourceLoader cache mode
13
+ ## reaches, and every script loaded after it that depends on it compiles in silence, reload() and all.
14
+ ## So a run ends at the first script that raised anything, and the rest go to a fresh engine.
15
+
16
+ const Read = preload("reading.gd")
17
+ const Log = preload("logger.gd")
18
+
19
+ ## Where the engine raises a script's compile errors from.
20
+ const RELOAD: String = "GDScript::reload"
21
+
22
+ var _log: Log
23
+
24
+
25
+ ## Every compile error the engine raises while it is attached, in order.
26
+ class Caught:
27
+ extends Logger
28
+
29
+ var raised: Array[Dictionary] = []
30
+
31
+ func _log_message(_message: String, _error: bool) -> void:
32
+ pass
33
+
34
+ # The engine's own signature for a Logger, eight parameters and not this project's to shape.
35
+ # gdlint:ignore = function-arguments-number
36
+ func _log_error(
37
+ function: String,
38
+ file: String,
39
+ line: int,
40
+ code: String,
41
+ rationale: String,
42
+ _editor_notify: bool,
43
+ _error_type: int,
44
+ _script_backtraces: Array[ScriptBacktrace],
45
+ ) -> void:
46
+ if function != RELOAD:
47
+ return
48
+ raised.append(
49
+ {"path": file, "line": line, "message": rationale if not rationale.is_empty() else code}
50
+ )
51
+
52
+
53
+ func _init(p_log: Log) -> void:
54
+ _log = p_log
55
+
56
+
57
+ ## Whether the scripts in [code]paths[/code] compile in a fresh engine, which reads the class cache
58
+ ## as it starts and so resolves every class the cache lists, up to and including the first that
59
+ ## raised an error. [code]checked[/code] names the scripts read, in order, and [code]failed[/code]
60
+ ## those that did not compile; [code]scripts[/code] holds each failed one with the errors raised in
61
+ ## its own file, and the other scripts that failed to compile while it was loaded. Read past the
62
+ ## resource cache, so a script loaded for an earlier one is compiled again for its own entry.
63
+ func check_scripts(params: Dictionary) -> Dictionary:
64
+ var asked: Array = params.get("paths", [])
65
+ var caught: Caught = Caught.new()
66
+ OS.add_logger(caught)
67
+ var checked: Array[String] = []
68
+ var failed: Array[String] = []
69
+ var scripts: Array[Dictionary] = []
70
+ for entry: Variant in asked:
71
+ var path: String = str(entry)
72
+ checked.append(path)
73
+ caught.raised.clear()
74
+ var loaded: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
75
+ var errors: Array[Dictionary] = []
76
+ var others: Array[String] = []
77
+ for error: Dictionary in caught.raised:
78
+ var at: String = str(error["path"])
79
+ if at != path:
80
+ if not at in others:
81
+ others.append(at)
82
+ continue
83
+ var found: Dictionary = {"line": Read.as_int(error["line"]), "message": str(error["message"])}
84
+ if not found in errors:
85
+ errors.append(found)
86
+ if loaded == null and errors.is_empty():
87
+ errors.append({"line": 0, "message": "The engine could not load it as a script."})
88
+ if errors.is_empty() and others.is_empty():
89
+ continue
90
+ if not errors.is_empty():
91
+ failed.append(path)
92
+ var one: Dictionary = {"path": path, "errors": errors}
93
+ if not others.is_empty():
94
+ one["dependencies_with_errors"] = others
95
+ scripts.append(one)
96
+ break
97
+ OS.remove_logger(caught)
98
+ return {"checked": checked, "failed": failed, "scripts": scripts}