gdharness 1.3.42 → 1.3.44
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 +383 -243
- package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
- package/build/godot/addons/gdharness_editor/animation_reads.gd +55 -33
- package/build/godot/addons/gdharness_editor/bridge_client.gd +53 -58
- package/build/godot/addons/gdharness_editor/declared_values.gd +68 -32
- package/build/godot/addons/gdharness_editor/file_reads.gd +83 -42
- package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
- package/build/godot/addons/gdharness_editor/headless_check.gd +68 -44
- package/build/godot/addons/gdharness_editor/plugin.gd +2 -3
- package/build/godot/addons/gdharness_editor/property_paths.gd +22 -12
- package/build/godot/addons/gdharness_editor/property_values.gd +143 -78
- package/build/godot/addons/gdharness_editor/reading.gd +51 -17
- package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
- package/build/godot/addons/gdharness_editor/scene_file.gd +127 -80
- package/build/godot/addons/gdharness_editor/scene_uids.gd +12 -6
- package/build/godot/addons/gdharness_editor/serialisation.gd +75 -51
- package/build/godot/addons/gdharness_editor/setting_reads.gd +7 -6
- package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -6
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +37 -27
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +79 -49
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +65 -42
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +24 -22
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +49 -29
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +26 -21
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +40 -40
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +166 -107
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -9
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +68 -41
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +35 -20
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +28 -13
- package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +12 -5
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +113 -67
- package/build/godot/addons/gdharness_editor/tools/script_paths.gd +20 -8
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +11 -8
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +47 -34
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +77 -50
- package/build/godot/addons/gdharness_editor/value_shapes.gd +5 -6
- package/build/godot/addons/gdharness_runtime/call_errors.gd +1 -2
- package/build/godot/addons/gdharness_runtime/declared_values.gd +68 -32
- package/build/godot/addons/gdharness_runtime/error_report.gd +1 -2
- package/build/godot/addons/gdharness_runtime/reading.gd +51 -17
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +149 -147
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +64 -42
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +184 -122
- package/build/godot/addons/gdharness_runtime/runtime_frames.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd +24 -20
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +134 -92
- package/build/godot/addons/gdharness_runtime/runtime_items.gd +29 -13
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +0 -1
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +34 -17
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +17 -13
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +84 -46
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +117 -79
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +14 -17
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +23 -16
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +117 -66
- package/build/godot/addons/gdharness_runtime/runtime_touch.gd +15 -15
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +44 -27
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +108 -73
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +111 -84
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +10 -7
- package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -51
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +5 -6
- package/build/godot/addons/gdharness_tests/bench.gd +4 -5
- package/build/godot/addons/gdharness_tests/event.gd +10 -5
- package/build/godot/addons/gdharness_tests/share.gd +38 -18
- package/build/godot/addons/gdharness_tests/suites.gd +3 -3
- package/build/godot/addons/gdharness_tests/wait.gd +3 -2
- package/build/godot/loader/loader.gd +9 -9
- package/build/godot/operations/animation_reads.gd +55 -33
- package/build/godot/operations/audio_buses.gd +48 -19
- package/build/godot/operations/class_cache.gd +16 -11
- package/build/godot/operations/classdb_queries.gd +89 -44
- package/build/godot/operations/declared_values.gd +68 -32
- package/build/godot/operations/dependencies.gd +190 -109
- package/build/godot/operations/editor_tools.gd +10 -10
- package/build/godot/operations/export_presets.gd +100 -54
- package/build/godot/operations/file_reads.gd +83 -42
- package/build/godot/operations/file_walk.gd +12 -5
- package/build/godot/operations/gdscript_analysis.gd +86 -65
- package/build/godot/operations/gdscript_authoring.gd +83 -36
- package/build/godot/operations/godot_operations.gd +17 -11
- package/build/godot/operations/import_pipeline.gd +110 -59
- package/build/godot/operations/input_actions.gd +62 -39
- package/build/godot/operations/patterns.gd +3 -1
- package/build/godot/operations/plugins.gd +36 -16
- package/build/godot/operations/project_config.gd +61 -32
- package/build/godot/operations/project_diagnostics.gd +24 -11
- package/build/godot/operations/project_file.gd +16 -16
- package/build/godot/operations/project_validation.gd +71 -44
- package/build/godot/operations/property_paths.gd +22 -12
- package/build/godot/operations/property_values.gd +143 -78
- package/build/godot/operations/reading.gd +51 -17
- package/build/godot/operations/rename_checks.gd +2 -2
- package/build/godot/operations/resource_files.gd +11 -9
- package/build/godot/operations/same_values.gd +4 -4
- package/build/godot/operations/scene_connections.gd +27 -13
- package/build/godot/operations/script_check.gd +52 -49
- package/build/godot/operations/serialisation.gd +75 -51
- package/build/godot/operations/setting_reads.gd +7 -6
- package/build/godot/operations/value_shapes.gd +5 -6
- package/build/godot/tests/listed.gd +321 -268
- package/build/keeper.js +43 -43
- package/package.json +1 -1
|
@@ -5,20 +5,19 @@ const GdhRead = preload("reading.gd")
|
|
|
5
5
|
const GdhFileWalk = preload("file_walk.gd")
|
|
6
6
|
const GdhLog = preload("logger.gd")
|
|
7
7
|
const GdhResourceFiles = preload("resource_files.gd")
|
|
8
|
-
|
|
9
8
|
## What a reverse search reads unless told otherwise: the resource and code files, and the settings
|
|
10
|
-
## files that name resources, "godot" for project.godot, "cfg" for plugin.cfg and
|
|
11
|
-
## and "import" for the sidecars that name import scripts and external
|
|
9
|
+
## files that name resources, "godot" for project.godot, "cfg" for plugin.cfg and
|
|
10
|
+
## export_presets.cfg, and "import" for the sidecars that name import scripts and external
|
|
11
|
+
## materials.
|
|
12
12
|
const PROJECT_FILE: String = "res://project.godot"
|
|
13
13
|
const DEFAULT_USAGE_KINDS: Array[String] = [
|
|
14
|
-
"tscn", "tres", "gd", "gdshader", "gdshaderinc", "godot", "cfg", "import"
|
|
14
|
+
"tscn", "tres", "gd", "gdshader", "gdshaderinc", "godot", "cfg", "import",
|
|
15
15
|
]
|
|
16
|
-
## The files written in the shader language, whose comments are // and /* */ rather than #, which in
|
|
17
|
-
## shader starts a preprocessor directive.
|
|
16
|
+
## The files written in the shader language, whose comments are // and /* */ rather than #, which in
|
|
17
|
+
## a shader starts a preprocessor directive.
|
|
18
18
|
const SHADER_KINDS: Array[String] = ["gdshader", "gdshaderinc"]
|
|
19
19
|
## One string literal of either quote, its contents captured by the one it opened with.
|
|
20
20
|
const STRING_LITERAL: String = "\"((?:[^\"\\\\]|\\\\.)*)\"|'((?:[^'\\\\]|\\\\.)*)'"
|
|
21
|
-
|
|
22
21
|
# What a dependency reference looks like in source, and how the path is read out of each form.
|
|
23
22
|
const REFERENCE_PATTERNS: Array[String] = [
|
|
24
23
|
"res://[^\"'\\s\\]\\)]+",
|
|
@@ -37,28 +36,8 @@ var _files: GdhFileWalk = GdhFileWalk.new()
|
|
|
37
36
|
## script again made the whole-project answer quadratic in the number of scripts.
|
|
38
37
|
var _classes_declared: Dictionary = {}
|
|
39
38
|
var _classes_read: bool = false
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# Everything one dependency walk carries: the settings it was started with, and the state it
|
|
43
|
-
# accumulates as it recurses. Kept together so the recursion passes one object rather than
|
|
44
|
-
# six positional arguments that have to stay in the same order at every call site.
|
|
45
|
-
class GdhDependencyWalk:
|
|
46
|
-
var max_depth: int
|
|
47
|
-
var include_built_in: bool
|
|
48
|
-
var visited: Dictionary = {}
|
|
49
|
-
var path_stack: Array[String] = []
|
|
50
|
-
var circular_references: Array
|
|
51
|
-
## Whether a file the depth stops at is read for how many files it names, which the walk doing
|
|
52
|
-
## that reading does not do again for those.
|
|
53
|
-
var counts_beyond: bool
|
|
54
|
-
|
|
55
|
-
func _init(
|
|
56
|
-
p_max_depth: int, p_include_built_in: bool, p_circular_references: Array, p_counts_beyond: bool = true
|
|
57
|
-
) -> void:
|
|
58
|
-
max_depth = p_max_depth
|
|
59
|
-
include_built_in = p_include_built_in
|
|
60
|
-
circular_references = p_circular_references
|
|
61
|
-
counts_beyond = p_counts_beyond
|
|
39
|
+
var _uids_declared: Dictionary = {}
|
|
40
|
+
var _uids_read: bool = false
|
|
62
41
|
|
|
63
42
|
|
|
64
43
|
func _init(p_log: GdhLog) -> void:
|
|
@@ -88,7 +67,9 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
88
67
|
return _log.failure(
|
|
89
68
|
_files.with_nearest_files("Resource file does not exist: " + full_path, full_path)
|
|
90
69
|
)
|
|
91
|
-
var walk: GdhDependencyWalk = GdhDependencyWalk.new(
|
|
70
|
+
var walk: GdhDependencyWalk = GdhDependencyWalk.new(
|
|
71
|
+
max_depth, include_built_in, circular_references
|
|
72
|
+
)
|
|
92
73
|
dependencies[full_path] = _analyze_resource(full_path, 0, walk)
|
|
93
74
|
|
|
94
75
|
var references: int = 0
|
|
@@ -105,7 +86,7 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
105
86
|
# `exists: false` among the entries, which a caller reading the summary never met.
|
|
106
87
|
var missing_files: Array[Dictionary] = []
|
|
107
88
|
for path: String in missing:
|
|
108
|
-
missing_files.append({"path": path, "referenced_by": missing[path]})
|
|
89
|
+
missing_files.append({ "path": path, "referenced_by": missing[path] })
|
|
109
90
|
|
|
110
91
|
return {
|
|
111
92
|
"dependencies": dependencies,
|
|
@@ -119,8 +100,8 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
119
100
|
"total_dependencies": distinct.size(),
|
|
120
101
|
"total_references": references,
|
|
121
102
|
"circular_count": circular_references.size(),
|
|
122
|
-
"missing": missing_files
|
|
123
|
-
}
|
|
103
|
+
"missing": missing_files,
|
|
104
|
+
},
|
|
124
105
|
}
|
|
125
106
|
|
|
126
107
|
|
|
@@ -137,10 +118,14 @@ func missing_references() -> Array[Dictionary]:
|
|
|
137
118
|
var naming: Array = missing.get_or_add(path, [])
|
|
138
119
|
if not file in naming:
|
|
139
120
|
naming.append(file)
|
|
140
|
-
for file: String in _files.find_files_with_extensions(
|
|
121
|
+
for file: String in _files.find_files_with_extensions(
|
|
122
|
+
"res://", GdhResourceFiles.TEXT_RESOURCE_KINDS
|
|
123
|
+
):
|
|
141
124
|
var content: String = FileAccess.get_file_as_string(file)
|
|
142
125
|
var moved: Dictionary = _moved_by_uid(content)
|
|
143
|
-
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(
|
|
126
|
+
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(
|
|
127
|
+
content
|
|
128
|
+
):
|
|
144
129
|
var at: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(line.get_string())
|
|
145
130
|
if at == null or moved.has(at.get_string(1)):
|
|
146
131
|
continue
|
|
@@ -162,7 +147,7 @@ func missing_references() -> Array[Dictionary]:
|
|
|
162
147
|
for path: String in paths:
|
|
163
148
|
var naming: Array = missing[path]
|
|
164
149
|
naming.sort()
|
|
165
|
-
found.append({"path": path, "referenced_by": naming})
|
|
150
|
+
found.append({ "path": path, "referenced_by": naming })
|
|
166
151
|
return found
|
|
167
152
|
|
|
168
153
|
|
|
@@ -183,12 +168,16 @@ func _loaded_and_missing(script: String) -> Array[String]:
|
|
|
183
168
|
continue
|
|
184
169
|
var calls: Dictionary = {}
|
|
185
170
|
for loading: RegExMatch in (
|
|
186
|
-
GdhPatterns
|
|
171
|
+
GdhPatterns
|
|
172
|
+
. compiled("(?:(?<![\\w.])|ResourceLoader\\.)load\\(\\s*(?=[\"'])")
|
|
173
|
+
. search_all(code)
|
|
187
174
|
):
|
|
188
175
|
calls[loading.get_end()] = true
|
|
189
176
|
for literal: RegExMatch in GdhPatterns.compiled(STRING_LITERAL).search_all(code):
|
|
190
177
|
var single: String = literal.get_string(2)
|
|
191
|
-
var path: String = _resolved(
|
|
178
|
+
var path: String = _resolved(
|
|
179
|
+
literal.get_string(1) if single.is_empty() else single, script
|
|
180
|
+
)
|
|
192
181
|
if calls.has(literal.get_start()):
|
|
193
182
|
loaded.append(path)
|
|
194
183
|
else:
|
|
@@ -200,7 +189,11 @@ func _loaded_and_missing(script: String) -> Array[String]:
|
|
|
200
189
|
if not named.has(path) and not missing.has(path):
|
|
201
190
|
missing.append(path)
|
|
202
191
|
continue
|
|
203
|
-
if
|
|
192
|
+
if (
|
|
193
|
+
not path.begins_with("res://")
|
|
194
|
+
or path.begins_with("res://.")
|
|
195
|
+
or path.get_extension().is_empty()
|
|
196
|
+
):
|
|
204
197
|
continue
|
|
205
198
|
if not named.has(path) and not FileAccess.file_exists(path) and not missing.has(path):
|
|
206
199
|
missing.append(path)
|
|
@@ -239,7 +232,11 @@ static func missing_line(entry: Dictionary) -> String:
|
|
|
239
232
|
var naming: PackedStringArray = PackedStringArray(referring)
|
|
240
233
|
return (
|
|
241
234
|
"%s does not exist, and %s %s to it"
|
|
242
|
-
% [
|
|
235
|
+
% [
|
|
236
|
+
str(entry.get("path", "")),
|
|
237
|
+
", ".join(naming),
|
|
238
|
+
"refers" if naming.size() == 1 else "refer",
|
|
239
|
+
]
|
|
243
240
|
)
|
|
244
241
|
|
|
245
242
|
|
|
@@ -276,7 +273,7 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
276
273
|
by_autoload = GdhPatterns.compiled("\\b" + _regex_escaped(autoload_named) + "\\b")
|
|
277
274
|
var target: Dictionary = {
|
|
278
275
|
"path": resource_path,
|
|
279
|
-
"uid": str(GdhResourceFiles.uid_of(resource_path).get("uid", "")) if exists else ""
|
|
276
|
+
"uid": str(GdhResourceFiles.uid_of(resource_path).get("uid", "")) if exists else "",
|
|
280
277
|
}
|
|
281
278
|
|
|
282
279
|
var all_files: Array[String] = _usage_files(file_types)
|
|
@@ -296,9 +293,9 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
296
293
|
var text: String = file.get_as_text()
|
|
297
294
|
file.close()
|
|
298
295
|
var lines: PackedStringArray = text.split("\n")
|
|
299
|
-
# A class is used by name only in a script's code. In a scene or a resource the same word is
|
|
300
|
-
# node's name or a caption, and each was counted as a use of the class; a shader has no
|
|
301
|
-
# for one at all.
|
|
296
|
+
# A class is used by name only in a script's code. In a scene or a resource the same word is
|
|
297
|
+
# a node's name or a caption, and each was counted as a use of the class; a shader has no
|
|
298
|
+
# use for one at all.
|
|
302
299
|
var code: bool = file_path.get_extension() == "gd"
|
|
303
300
|
# In a shader # opens a directive, so an #include was answered as a comment, and its
|
|
304
301
|
# comments are // and /* */.
|
|
@@ -326,9 +323,13 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
326
323
|
var trimmed: String = line.strip_edges()
|
|
327
324
|
var kind: String = ""
|
|
328
325
|
var extra: Dictionary = {}
|
|
329
|
-
var loaded: String =
|
|
326
|
+
var loaded: String = (
|
|
327
|
+
_loaded_by_uid(line) if trimmed.begins_with("[ext_resource") else ""
|
|
328
|
+
)
|
|
330
329
|
var written: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(line)
|
|
331
|
-
var written_path: String =
|
|
330
|
+
var written_path: String = (
|
|
331
|
+
"" if written == null else _resolved(written.get_string(1), file_path)
|
|
332
|
+
)
|
|
332
333
|
# A mention in a comment is not a use, and both were answered as one. The question a
|
|
333
334
|
# reverse walk is asked is whether anything still uses a class, and a project that
|
|
334
335
|
# documents itself names its collaborators in `##` links: four references to one class,
|
|
@@ -339,10 +340,14 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
339
340
|
kind = "doc"
|
|
340
341
|
elif (code and trimmed.begins_with("#")) or name_in_comment:
|
|
341
342
|
kind = "comment"
|
|
342
|
-
elif
|
|
343
|
+
elif (
|
|
344
|
+
not shader_lines.is_empty()
|
|
345
|
+
and not _names_target(shader_lines[i], file_path, target)
|
|
346
|
+
):
|
|
343
347
|
kind = "comment"
|
|
344
348
|
elif by_its_path and not loaded.is_empty() and loaded != target["path"]:
|
|
345
|
-
# The engine loads the file the uid names, so the path written here is used by
|
|
349
|
+
# The engine loads the file the uid names, so the path written here is used by
|
|
350
|
+
# nothing.
|
|
346
351
|
kind = "stale_path"
|
|
347
352
|
extra["loads"] = loaded
|
|
348
353
|
elif by_its_path:
|
|
@@ -362,7 +367,7 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
362
367
|
if GdhPatterns.without_annotations(line).begins_with("extends ")
|
|
363
368
|
else "class_name"
|
|
364
369
|
)
|
|
365
|
-
var found: Dictionary = {"line": i + 1, "kind": kind, "text": trimmed}
|
|
370
|
+
var found: Dictionary = { "line": i + 1, "kind": kind, "text": trimmed }
|
|
366
371
|
found.merge(extra)
|
|
367
372
|
references.append(found)
|
|
368
373
|
by_kind[kind] = GdhRead.as_int(by_kind.get(kind, 0)) + 1
|
|
@@ -370,13 +375,16 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
370
375
|
in_code += 1
|
|
371
376
|
|
|
372
377
|
if not references.is_empty():
|
|
373
|
-
usages.append({"file": file_path, "references": references})
|
|
378
|
+
usages.append({ "file": file_path, "references": references })
|
|
374
379
|
total += references.size()
|
|
375
380
|
|
|
376
381
|
if not exists and usages.is_empty():
|
|
377
382
|
return _log.failure(
|
|
378
383
|
_files.with_nearest_files(
|
|
379
|
-
|
|
384
|
+
(
|
|
385
|
+
"Resource file does not exist: %s, and nothing in the project refers to it"
|
|
386
|
+
% resource_path
|
|
387
|
+
),
|
|
380
388
|
resource_path
|
|
381
389
|
)
|
|
382
390
|
)
|
|
@@ -413,7 +421,9 @@ func _declared_class_name(path: String) -> String:
|
|
|
413
421
|
return ""
|
|
414
422
|
while not file.eof_reached():
|
|
415
423
|
# Annotations first, since `@abstract class_name X` is one line and this read it as none.
|
|
416
|
-
var found: RegExMatch = GdhPatterns.declared_class(
|
|
424
|
+
var found: RegExMatch = GdhPatterns.declared_class(
|
|
425
|
+
GdhPatterns.without_annotations(file.get_line())
|
|
426
|
+
)
|
|
417
427
|
if found != null:
|
|
418
428
|
file.close()
|
|
419
429
|
return found.get_string(1)
|
|
@@ -443,11 +453,11 @@ func _usage_files(kinds: Array) -> Array[String]:
|
|
|
443
453
|
return found
|
|
444
454
|
|
|
445
455
|
|
|
446
|
-
# Whether [param line], in [param file_path], names the target by path or by UID. Every string on
|
|
447
|
-
# line is read as a path the way the engine reads it: a res:// path as it is, a uid:// by what
|
|
448
|
-
# names, and anything else relative to the file it is in, which is how `preload("sibling.gd")`
|
|
449
|
-
# plugin.cfg's `script="plugin.gd"` name their files. Only the quoted form of the project path
|
|
450
|
-
# looked for, so neither of those was found, and nor was a scene named by its UID.
|
|
456
|
+
# Whether [param line], in [param file_path], names the target by path or by UID. Every string on
|
|
457
|
+
# the line is read as a path the way the engine reads it: a res:// path as it is, a uid:// by what
|
|
458
|
+
# it names, and anything else relative to the file it is in, which is how `preload("sibling.gd")`
|
|
459
|
+
# and a plugin.cfg's `script="plugin.gd"` name their files. Only the quoted form of the project path
|
|
460
|
+
# was looked for, so neither of those was found, and nor was a scene named by its UID.
|
|
451
461
|
func _names_target(line: String, file_path: String, target: Dictionary) -> bool:
|
|
452
462
|
var path: String = target["path"]
|
|
453
463
|
var uid: String = target["uid"]
|
|
@@ -566,15 +576,17 @@ func _count_recursive(deps: Array[Dictionary]) -> int:
|
|
|
566
576
|
return count
|
|
567
577
|
|
|
568
578
|
|
|
569
|
-
func _analyze_resource(
|
|
579
|
+
func _analyze_resource(
|
|
580
|
+
path: String, current_depth: int, walk: GdhDependencyWalk
|
|
581
|
+
) -> Array[Dictionary]:
|
|
570
582
|
var deps: Array[Dictionary] = []
|
|
571
583
|
|
|
572
584
|
if current_depth >= walk.max_depth:
|
|
573
585
|
return deps
|
|
574
586
|
|
|
575
|
-
# Reused only when it was walked with as much depth left as now. A resource first reached deep
|
|
576
|
-
# the walk has a list cut short at the limit, and handing that to a nearer reach answered
|
|
577
|
-
# chain that stopped where nothing stopped it.
|
|
587
|
+
# Reused only when it was walked with as much depth left as now. A resource first reached deep
|
|
588
|
+
# in the walk has a list cut short at the limit, and handing that to a nearer reach answered
|
|
589
|
+
# with a chain that stopped where nothing stopped it.
|
|
578
590
|
var remaining: int = walk.max_depth - current_depth
|
|
579
591
|
if walk.visited.has(path):
|
|
580
592
|
var cached: Dictionary = walk.visited[path]
|
|
@@ -589,8 +601,9 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
589
601
|
var content: String = file.get_as_text()
|
|
590
602
|
file.close()
|
|
591
603
|
# A script's comments name paths as examples, and read as references they were answered as
|
|
592
|
-
# dependencies, one of a file that does not exist: the shipped serialisation module's
|
|
593
|
-
# about `["res://addons/x/plugin.cfg"]` was a dependency of every project with the
|
|
604
|
+
# dependencies, one of a file that does not exist: the shipped serialisation module's
|
|
605
|
+
# comment about `["res://addons/x/plugin.cfg"]` was a dependency of every project with the
|
|
606
|
+
# addon.
|
|
594
607
|
if path.get_extension() == "gd":
|
|
595
608
|
content = _code_lines(content)
|
|
596
609
|
elif path.get_extension() in SHADER_KINDS:
|
|
@@ -604,8 +617,8 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
604
617
|
var written: String = _resolved(_referenced_path(m.get_string()), path)
|
|
605
618
|
var dep_path: String = str(moved.get(written, written))
|
|
606
619
|
|
|
607
|
-
# A uid the project does not know names no file, and loading it fails: passed over
|
|
608
|
-
# no res:// path, a script's load of one went unsaid while the game printed
|
|
620
|
+
# A uid the project does not know names no file, and loading it fails: passed over
|
|
621
|
+
# as no res:// path, a script's load of one went unsaid while the game printed
|
|
609
622
|
# "Unrecognized UID" at that line.
|
|
610
623
|
if dep_path.begins_with("uid://"):
|
|
611
624
|
_depend(deps, path, dep_path, {}, current_depth, walk)
|
|
@@ -613,7 +626,8 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
613
626
|
if not dep_path.begins_with("res://"):
|
|
614
627
|
continue
|
|
615
628
|
# A literal naming a directory, or the start of a path a script builds as it runs
|
|
616
|
-
# ("res://levels/" + name), names no file, and walked as one it was a missing
|
|
629
|
+
# ("res://levels/" + name), names no file, and walked as one it was a missing
|
|
630
|
+
# dependency.
|
|
617
631
|
if dep_path.get_extension().is_empty():
|
|
618
632
|
continue
|
|
619
633
|
|
|
@@ -623,32 +637,34 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
623
637
|
if not walk.include_built_in and dep_path.begins_with("res://."):
|
|
624
638
|
continue
|
|
625
639
|
|
|
626
|
-
var named_by: Dictionary = {"stale_path": written} if dep_path != written else {}
|
|
640
|
+
var named_by: Dictionary = { "stale_path": written } if dep_path != written else {}
|
|
627
641
|
if unknown.has(dep_path):
|
|
628
642
|
named_by["unknown_uid"] = unknown[dep_path]
|
|
629
643
|
_depend(deps, path, dep_path, named_by, current_depth, walk)
|
|
630
644
|
|
|
631
|
-
# A script uses another by its class_name as surely as by a preload, and only the reverse
|
|
632
|
-
# looked for that: asked what a script depends on, the answer left out the script it
|
|
633
|
-
# variable with, while asking what used that script named this one. An autoload's
|
|
634
|
-
# name is the same kind of use, of the script registered under it.
|
|
645
|
+
# A script uses another by its class_name as surely as by a preload, and only the reverse
|
|
646
|
+
# walk looked for that: asked what a script depends on, the answer left out the script it
|
|
647
|
+
# typed a variable with, while asking what used that script named this one. An autoload's
|
|
648
|
+
# global name is the same kind of use, of the script registered under it.
|
|
635
649
|
if path.get_extension() == "gd":
|
|
636
650
|
var classes: Dictionary = classes_declared()
|
|
637
651
|
for used: String in classes_used_in(content, classes):
|
|
638
|
-
_depend(deps, path, str(classes[used]), {"by_class": used}, current_depth, walk)
|
|
652
|
+
_depend(deps, path, str(classes[used]), { "by_class": used }, current_depth, walk)
|
|
639
653
|
var autoloads: Dictionary = autoloads_declared()
|
|
640
654
|
for used: String in classes_used_in(content, autoloads):
|
|
641
|
-
_depend(
|
|
655
|
+
_depend(
|
|
656
|
+
deps, path, str(autoloads[used]), { "by_autoload": used }, current_depth, walk
|
|
657
|
+
)
|
|
642
658
|
|
|
643
659
|
walk.path_stack.pop_back()
|
|
644
|
-
walk.visited[path] = {"deps": deps, "remaining": remaining}
|
|
660
|
+
walk.visited[path] = { "deps": deps, "remaining": remaining }
|
|
645
661
|
return deps
|
|
646
662
|
|
|
647
663
|
|
|
648
|
-
# Adds [param dep_path] to [param deps], what [param path] depends on, walked as far as the depth
|
|
649
|
-
# allows, once however many times it is named. [param named_by] is the class or autoload name
|
|
650
|
-
# named by, as by_class or by_autoload, and the path written when the engine loads another,
|
|
651
|
-
# stale_path; empty for a path that is the file.
|
|
664
|
+
# Adds [param dep_path] to [param deps], what [param path] depends on, walked as far as the depth
|
|
665
|
+
# left allows, once however many times it is named. [param named_by] is the class or autoload name
|
|
666
|
+
# it was named by, as by_class or by_autoload, and the path written when the engine loads another,
|
|
667
|
+
# as stale_path; empty for a path that is the file.
|
|
652
668
|
func _depend(
|
|
653
669
|
deps: Array[Dictionary],
|
|
654
670
|
path: String,
|
|
@@ -662,14 +678,15 @@ func _depend(
|
|
|
662
678
|
for existing: Dictionary in deps:
|
|
663
679
|
if existing.get("path", "") == dep_path:
|
|
664
680
|
return
|
|
665
|
-
# A uid still written as one is one the project does not know, and asked about, the engine
|
|
666
|
-
# an error for it.
|
|
681
|
+
# A uid still written as one is one the project does not know, and asked about, the engine
|
|
682
|
+
# prints an error for it.
|
|
667
683
|
var dep_info: Dictionary = {
|
|
668
|
-
"path": dep_path,
|
|
684
|
+
"path": dep_path,
|
|
685
|
+
"exists": not dep_path.begins_with("uid://") and FileAccess.file_exists(dep_path),
|
|
669
686
|
}
|
|
670
687
|
dep_info.merge(named_by)
|
|
671
|
-
# Marked where the reference closing the cycle is. Marked inside the file reached again, the
|
|
672
|
-
# answered that file as a dependency of itself.
|
|
688
|
+
# Marked where the reference closing the cycle is. Marked inside the file reached again, the
|
|
689
|
+
# tree answered that file as a dependency of itself.
|
|
673
690
|
if dep_path in walk.path_stack:
|
|
674
691
|
var cycle: Array[String] = walk.path_stack.slice(walk.path_stack.find(dep_path))
|
|
675
692
|
cycle.append(dep_path)
|
|
@@ -683,8 +700,8 @@ func _depend(
|
|
|
683
700
|
if sub_deps.size() > 0:
|
|
684
701
|
dep_info["dependencies"] = sub_deps
|
|
685
702
|
elif dep_info["exists"] and walk.counts_beyond:
|
|
686
|
-
# A file the depth stopped at read the same as one naming nothing, and a missing file past
|
|
687
|
-
# went unsaid.
|
|
703
|
+
# A file the depth stopped at read the same as one naming nothing, and a missing file past
|
|
704
|
+
# it went unsaid.
|
|
688
705
|
var beyond: GdhDependencyWalk = GdhDependencyWalk.new(1, walk.include_built_in, [], false)
|
|
689
706
|
var named: int = _analyze_resource(dep_path, 0, beyond).size()
|
|
690
707
|
if named > 0:
|
|
@@ -694,7 +711,7 @@ func _depend(
|
|
|
694
711
|
|
|
695
712
|
## Every autoload the project registers as a global name, with its script. One that is not global
|
|
696
713
|
## is no name code can use.
|
|
697
|
-
|
|
714
|
+
func autoloads_declared() -> Dictionary:
|
|
698
715
|
var autoloads: Dictionary = {}
|
|
699
716
|
for property: Dictionary in ProjectSettings.get_property_list():
|
|
700
717
|
var setting: String = str(property.get("name", ""))
|
|
@@ -728,12 +745,12 @@ func list_script_classes(_params: Dictionary) -> Dictionary:
|
|
|
728
745
|
names.sort()
|
|
729
746
|
var classes: Array[Dictionary] = []
|
|
730
747
|
for name: Variant in names:
|
|
731
|
-
classes.append({"name": str(name), "path": str(declared[name])})
|
|
732
|
-
return {"classes": classes, "count": classes.size()}
|
|
748
|
+
classes.append({ "name": str(name), "path": str(declared[name]) })
|
|
749
|
+
return { "classes": classes, "count": classes.size() }
|
|
733
750
|
|
|
734
751
|
|
|
735
|
-
## The names in [param classes] that the code in [param content] uses, in the order first used:
|
|
736
|
-
## outside strings and comments, so a class named in prose or in a message is not a use of it.
|
|
752
|
+
## The names in [param classes] that the code in [param content] uses, in the order first used:
|
|
753
|
+
## words outside strings and comments, so a class named in prose or in a message is not a use of it.
|
|
737
754
|
static func classes_used_in(content: String, classes: Dictionary) -> Array[String]:
|
|
738
755
|
var used: Array[String] = []
|
|
739
756
|
if classes.is_empty():
|
|
@@ -747,26 +764,63 @@ static func classes_used_in(content: String, classes: Dictionary) -> Array[Strin
|
|
|
747
764
|
return used
|
|
748
765
|
|
|
749
766
|
|
|
750
|
-
# [param written] as the res:// path the engine would load from [param from]: a uid:// by
|
|
751
|
-
#
|
|
752
|
-
#
|
|
753
|
-
|
|
754
|
-
static func _resolved(written: String, from: String) -> String:
|
|
767
|
+
# [param written] as the res:// path the engine would load from [param from]: a uid:// by the file
|
|
768
|
+
# it names, and a relative path against the referring file's directory. Both were passed over as not
|
|
769
|
+
# being res:// paths, so a script's `preload("sibling.gd")` was no dependency at all.
|
|
770
|
+
func _resolved(written: String, from: String) -> String:
|
|
755
771
|
if written.begins_with("res://"):
|
|
756
772
|
return written
|
|
757
773
|
if written.begins_with("uid://"):
|
|
758
|
-
var
|
|
759
|
-
return
|
|
774
|
+
var found: String = _uid_path(written)
|
|
775
|
+
return written if found.is_empty() else found
|
|
760
776
|
if written.contains("://"):
|
|
761
777
|
return written
|
|
762
778
|
return from.get_base_dir().path_join(written).simplify_path()
|
|
763
779
|
|
|
764
780
|
|
|
781
|
+
# The file the uid:// [param text] names, or empty when nothing does: the engine's uid cache first,
|
|
782
|
+
# then the uids the project's files declare. Only an import writes the cache, so on a fresh clone
|
|
783
|
+
# every uid was answered as naming no file, and a scene moved outside the editor was missing under
|
|
784
|
+
# its old path, although the first import loads it by its uid. A cached path with no file there
|
|
785
|
+
# gives way to a file declaring the uid, and is the answer when none does.
|
|
786
|
+
func _uid_path(text: String) -> String:
|
|
787
|
+
var id: int = ResourceUID.text_to_id(text)
|
|
788
|
+
if id == ResourceUID.INVALID_ID:
|
|
789
|
+
return ""
|
|
790
|
+
var cached: String = ResourceUID.get_id_path(id) if ResourceUID.has_id(id) else ""
|
|
791
|
+
if FileAccess.file_exists(cached):
|
|
792
|
+
return cached
|
|
793
|
+
return str(_files_by_uid().get(id, cached))
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
# Every file of the project that declares a uid, by its uid's id: in a .uid sidecar, an import
|
|
797
|
+
# sidecar or the header of a text resource, as GdhResourceFiles reads them. Read once for the call,
|
|
798
|
+
# the first time the cache does not know a uid.
|
|
799
|
+
func _files_by_uid() -> Dictionary:
|
|
800
|
+
if _uids_read:
|
|
801
|
+
return _uids_declared
|
|
802
|
+
_uids_read = true
|
|
803
|
+
var declaring: Dictionary = {}
|
|
804
|
+
for file: String in _files.find_files_with_extensions(
|
|
805
|
+
"res://", ["uid", "import"] + GdhResourceFiles.TEXT_RESOURCE_KINDS
|
|
806
|
+
):
|
|
807
|
+
var sidecar: bool = file.get_extension() in ["uid", "import"]
|
|
808
|
+
declaring[file.get_basename() if sidecar else file] = true
|
|
809
|
+
for file: String in declaring:
|
|
810
|
+
if not FileAccess.file_exists(file):
|
|
811
|
+
continue
|
|
812
|
+
var id: int = ResourceUID.text_to_id(str(GdhResourceFiles.uid_of(file).get("uid", "")))
|
|
813
|
+
if id != ResourceUID.INVALID_ID:
|
|
814
|
+
_uids_declared[id] = file
|
|
815
|
+
return _uids_declared
|
|
816
|
+
|
|
817
|
+
|
|
765
818
|
# The ext_resource paths in [param content] whose uid leads elsewhere, each with the file the uid
|
|
766
819
|
# names. The engine loads an ext_resource by its uid whenever the uid is known, whatever the path
|
|
767
|
-
# says, so a file moved outside the editor still loads, and the walk called it missing; every
|
|
768
|
-
# reads the old path, the bare res:// one included, so the paths are mapped before any of
|
|
769
|
-
|
|
820
|
+
# says, so a file moved outside the editor still loads, and the walk called it missing; every
|
|
821
|
+
# pattern reads the old path, the bare res:// one included, so the paths are mapped before any of
|
|
822
|
+
# them runs.
|
|
823
|
+
func _moved_by_uid(content: String) -> Dictionary:
|
|
770
824
|
var moved: Dictionary = {}
|
|
771
825
|
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
|
|
772
826
|
var found: String = _loaded_by_uid(line.get_string())
|
|
@@ -779,7 +833,7 @@ static func _moved_by_uid(content: String) -> Dictionary:
|
|
|
779
833
|
# The ext_resource paths in [param content], read from [param from], whose uid names no file the
|
|
780
834
|
# project has, each with that uid. The engine warns that the uid is invalid and loads the path, and
|
|
781
835
|
# the walk followed the path saying nothing, so an export was the first to mention it.
|
|
782
|
-
|
|
836
|
+
func _unknown_uids(content: String, from: String) -> Dictionary:
|
|
783
837
|
var unknown: Dictionary = {}
|
|
784
838
|
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
|
|
785
839
|
var uid: String = _unknown_uid_of(line.get_string())
|
|
@@ -790,21 +844,18 @@ static func _unknown_uids(content: String, from: String) -> Dictionary:
|
|
|
790
844
|
|
|
791
845
|
|
|
792
846
|
# The uid an ext_resource [param entry] carries when it names no file the project has, or empty.
|
|
793
|
-
|
|
847
|
+
func _unknown_uid_of(entry: String) -> String:
|
|
794
848
|
var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
|
|
795
849
|
return "" if uid == null or not _loaded_by_uid(entry).is_empty() else uid.get_string(1)
|
|
796
850
|
|
|
797
851
|
|
|
798
852
|
# The file an ext_resource [param entry] is loaded from by its uid, or empty when it carries none
|
|
799
853
|
# the project knows, and the engine falls back to the path.
|
|
800
|
-
|
|
854
|
+
func _loaded_by_uid(entry: String) -> String:
|
|
801
855
|
var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
|
|
802
856
|
if uid == null:
|
|
803
857
|
return ""
|
|
804
|
-
var
|
|
805
|
-
if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id):
|
|
806
|
-
return ""
|
|
807
|
-
var found: String = ResourceUID.get_id_path(id)
|
|
858
|
+
var found: String = _uid_path(uid.get_string(1))
|
|
808
859
|
return found if FileAccess.file_exists(found) else ""
|
|
809
860
|
|
|
810
861
|
|
|
@@ -838,8 +889,8 @@ static func _shader_code(content: String) -> String:
|
|
|
838
889
|
return "".join(PackedStringArray(kept))
|
|
839
890
|
|
|
840
891
|
|
|
841
|
-
# The path inside a preload(), load(), #include or ext_resource match; a bare res:// match is the
|
|
842
|
-
# An ext_resource is read first, since its type or its path can hold the word load.
|
|
892
|
+
# The path inside a preload(), load(), #include or ext_resource match; a bare res:// match is the
|
|
893
|
+
# path. An ext_resource is read first, since its type or its path can hold the word load.
|
|
843
894
|
func _referenced_path(matched: String) -> String:
|
|
844
895
|
var expression: String = ""
|
|
845
896
|
if matched.begins_with("ext_resource"):
|
|
@@ -851,3 +902,33 @@ func _referenced_path(matched: String) -> String:
|
|
|
851
902
|
var inner: RegEx = GdhPatterns.compiled(expression)
|
|
852
903
|
var inner_match: RegExMatch = inner.search(matched)
|
|
853
904
|
return inner_match.get_string(1) if inner_match else matched
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
# Everything one dependency walk carries: the settings it was started with, and the state it
|
|
908
|
+
# accumulates as it recurses. Kept together so the recursion passes one object rather than
|
|
909
|
+
# six positional arguments that have to stay in the same order at every call site.
|
|
910
|
+
class GdhDependencyWalk:
|
|
911
|
+
var max_depth: int
|
|
912
|
+
|
|
913
|
+
var include_built_in: bool
|
|
914
|
+
|
|
915
|
+
var visited: Dictionary = {}
|
|
916
|
+
|
|
917
|
+
var path_stack: Array[String] = []
|
|
918
|
+
|
|
919
|
+
var circular_references: Array
|
|
920
|
+
|
|
921
|
+
## Whether a file the depth stops at is read for how many files it names, which the walk doing
|
|
922
|
+
## that reading does not do again for those.
|
|
923
|
+
var counts_beyond: bool
|
|
924
|
+
|
|
925
|
+
func _init(
|
|
926
|
+
p_max_depth: int,
|
|
927
|
+
p_include_built_in: bool,
|
|
928
|
+
p_circular_references: Array,
|
|
929
|
+
p_counts_beyond: bool = true
|
|
930
|
+
) -> void:
|
|
931
|
+
max_depth = p_max_depth
|
|
932
|
+
include_built_in = p_include_built_in
|
|
933
|
+
circular_references = p_circular_references
|
|
934
|
+
counts_beyond = p_counts_beyond
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
|
-
|
|
3
2
|
## The editor addon's scene and resource tools, run by a headless engine for a project no editor
|
|
4
3
|
## serves.
|
|
5
4
|
##
|
|
@@ -16,7 +15,6 @@ const GdhSignalTools = preload("../addons/gdharness_editor/tools/signal_tools.gd
|
|
|
16
15
|
const GdhTilesetTools = preload("../addons/gdharness_editor/tools/tileset_tools.gd")
|
|
17
16
|
const GdhSceneFile = preload("../addons/gdharness_editor/scene_file.gd")
|
|
18
17
|
const GdhSceneUids = preload("../addons/gdharness_editor/scene_uids.gd")
|
|
19
|
-
|
|
20
18
|
const SCENE_COMMANDS: Array[String] = [
|
|
21
19
|
"create_scene",
|
|
22
20
|
"save_scene",
|
|
@@ -58,7 +56,7 @@ static func run(params: Dictionary) -> Dictionary:
|
|
|
58
56
|
var command: String = str(params.get("command", ""))
|
|
59
57
|
var given: Variant = params.get("arguments", {})
|
|
60
58
|
if not given is Dictionary:
|
|
61
|
-
return {"ok": false, "error": "arguments must be an object."}
|
|
59
|
+
return { "ok": false, "error": "arguments must be an object." }
|
|
62
60
|
var arguments: Dictionary = given
|
|
63
61
|
var tools: Node = null
|
|
64
62
|
if command in SCENE_COMMANDS:
|
|
@@ -76,7 +74,7 @@ static func run(params: Dictionary) -> Dictionary:
|
|
|
76
74
|
elif command in ANIMATION_STATE_COMMANDS:
|
|
77
75
|
tools = GdhAnimationStates.new()
|
|
78
76
|
else:
|
|
79
|
-
return {"ok": false, "error": "No editor tool runs headless as " + command}
|
|
77
|
+
return { "ok": false, "error": "No editor tool runs headless as " + command }
|
|
80
78
|
var answer: Variant = tools.call(command, arguments)
|
|
81
79
|
tools.free()
|
|
82
80
|
if answer is Dictionary:
|
|
@@ -86,19 +84,21 @@ static func run(params: Dictionary) -> Dictionary:
|
|
|
86
84
|
if not GdhSceneUids.by_path.is_empty():
|
|
87
85
|
answered["namedByPath"] = (
|
|
88
86
|
(
|
|
89
|
-
"What this saved names %s by path rather than by uid, which loads the same. A
|
|
90
|
-
+ "
|
|
91
|
-
+ "%s yet, as with a file made since the project was last scanned, so
|
|
92
|
-
+ "now would warn on every load. project_import refresh_uids
|
|
93
|
-
+ "writes after it name %s by uid."
|
|
87
|
+
"What this saved names %s by path rather than by uid, which loads the same. A "
|
|
88
|
+
+ "reference by uid is resolved through the project's uid cache, which holds "
|
|
89
|
+
+ "none for %s yet, as with a file made since the project was last scanned, so "
|
|
90
|
+
+ "naming %s by uid now would warn on every load. project_import refresh_uids "
|
|
91
|
+
+ "scans the project, and writes after it name %s by uid."
|
|
94
92
|
)
|
|
95
93
|
% [
|
|
96
94
|
", ".join(GdhSceneUids.by_path),
|
|
97
95
|
"it" if GdhSceneUids.by_path.size() == 1 else "them",
|
|
98
96
|
"it" if GdhSceneUids.by_path.size() == 1 else "them",
|
|
97
|
+
"it" if GdhSceneUids.by_path.size() == 1 else "them",
|
|
99
98
|
]
|
|
100
99
|
)
|
|
101
100
|
return answered
|
|
102
101
|
return {
|
|
103
|
-
"ok": false,
|
|
102
|
+
"ok": false,
|
|
103
|
+
"error": "%s answered %s rather than an object." % [command, type_string(typeof(answer))],
|
|
104
104
|
}
|