gdharness 0.12.17 → 0.13.0

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.
Files changed (36) hide show
  1. package/build/cli.js +174 -56
  2. package/build/godot/addons/auto_reload/auto_reload.gd +9 -2
  3. package/build/godot/addons/gdharness_editor/bridge_client.gd +11 -5
  4. package/build/godot/addons/gdharness_editor/plugin.gd +17 -4
  5. package/build/godot/addons/gdharness_editor/reading.gd +64 -0
  6. package/build/godot/addons/gdharness_editor/reading.gd.uid +1 -0
  7. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +30 -12
  8. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +5 -2
  9. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +3 -1
  10. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +50 -27
  11. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +92 -42
  12. package/build/godot/addons/gdharness_runtime/reading.gd +64 -0
  13. package/build/godot/addons/gdharness_runtime/reading.gd.uid +1 -0
  14. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +24 -10
  15. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +7 -5
  16. package/build/godot/addons/gdharness_runtime/runtime_input.gd +24 -22
  17. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +16 -13
  18. package/build/godot/addons/gdharness_runtime/runtime_values.gd +24 -8
  19. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +7 -4
  20. package/build/godot/operations/audio_buses.gd +7 -6
  21. package/build/godot/operations/class_cache.gd +14 -9
  22. package/build/godot/operations/classdb_queries.gd +4 -3
  23. package/build/godot/operations/dependencies.gd +19 -16
  24. package/build/godot/operations/file_walk.gd +4 -2
  25. package/build/godot/operations/gdscript_analysis.gd +2 -3
  26. package/build/godot/operations/gdscript_authoring.gd +20 -8
  27. package/build/godot/operations/godot_operations.gd +7 -2
  28. package/build/godot/operations/import_pipeline.gd +5 -4
  29. package/build/godot/operations/input_actions.gd +13 -9
  30. package/build/godot/operations/patterns.gd +22 -0
  31. package/build/godot/operations/plugins.gd +10 -6
  32. package/build/godot/operations/project_config.gd +41 -3
  33. package/build/godot/operations/reading.gd +66 -0
  34. package/build/godot/operations/serialisation.gd +41 -9
  35. package/build/index.js +147 -53
  36. package/package.json +1 -1
@@ -1,5 +1,7 @@
1
1
  extends RefCounted
2
2
 
3
+ const Patterns = preload("patterns.gd")
4
+ const Read = preload("reading.gd")
3
5
  const FileWalk = preload("file_walk.gd")
4
6
  const Log = preload("logger.gd")
5
7
 
@@ -39,9 +41,9 @@ func _init(p_log: Log) -> void:
39
41
  func get_dependencies(params: Dictionary) -> Dictionary:
40
42
  var resource_path: String = str(params.get("resource_path", ""))
41
43
  # No depth, or a depth of zero or less, means the whole chain; the walk stops at cycles.
42
- var depth: int = int(params.get("depth", 0))
44
+ var depth: int = Read.as_int(params.get("depth", 0))
43
45
  var max_depth: int = depth if depth > 0 else 1000
44
- var include_built_in: bool = bool(params.get("include_builtin", false))
46
+ var include_built_in: bool = Read.as_bool(params.get("include_builtin", false))
45
47
 
46
48
  _log.info(
47
49
  (
@@ -83,7 +85,10 @@ func get_dependencies(params: Dictionary) -> Dictionary:
83
85
 
84
86
  var dep_count: int = 0
85
87
  for key: String in dependencies:
86
- dep_count += _count_recursive(dependencies[key])
88
+ # Through a typed local: what comes out of a Dictionary is Variant, and a project that
89
+ # errors on handing one to a typed parameter will not compile this script at all.
90
+ var walked: Array[Dictionary] = dependencies[key]
91
+ dep_count += _count_recursive(walked)
87
92
 
88
93
  return {
89
94
  "dependencies": dependencies,
@@ -111,12 +116,10 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
111
116
  _log.info("Finding usages of: " + resource_path)
112
117
 
113
118
  var class_name_declared: String = _declared_class_name(resource_path)
114
- var by_path: RegEx = RegEx.new()
115
- by_path.compile('"(res://)?' + _regex_escaped(resource_path.substr(6)) + '"')
119
+ var by_path: RegEx = Patterns.compiled('"(res://)?' + _regex_escaped(resource_path.substr(6)) + '"')
116
120
  var by_class: RegEx = null
117
121
  if not class_name_declared.is_empty():
118
- by_class = RegEx.new()
119
- by_class.compile("\\b" + _regex_escaped(class_name_declared) + "\\b")
122
+ by_class = Patterns.compiled("\\b" + _regex_escaped(class_name_declared) + "\\b")
120
123
 
121
124
  var all_files: Array[String] = []
122
125
  for ext: Variant in file_types:
@@ -146,7 +149,7 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
146
149
  if kind.is_empty():
147
150
  continue
148
151
  references.append({"line": i + 1, "kind": kind, "text": line.strip_edges()})
149
- by_kind[kind] = int(by_kind.get(kind, 0)) + 1
152
+ by_kind[kind] = Read.as_int(by_kind.get(kind, 0)) + 1
150
153
 
151
154
  if not references.is_empty():
152
155
  usages.append({"file": file_path, "references": references})
@@ -176,8 +179,7 @@ func _declared_class_name(path: String) -> String:
176
179
  var file: FileAccess = FileAccess.open(path, FileAccess.READ)
177
180
  if not file:
178
181
  return ""
179
- var declaration: RegEx = RegEx.new()
180
- declaration.compile("^class_name\\s+([A-Za-z_][A-Za-z0-9_]*)")
182
+ var declaration: RegEx = Patterns.compiled("^class_name\\s+([A-Za-z_][A-Za-z0-9_]*)")
181
183
  while not file.eof_reached():
182
184
  var found: RegExMatch = declaration.search(file.get_line())
183
185
  if found != null:
@@ -214,7 +216,8 @@ func _count_recursive(deps: Array[Dictionary]) -> int:
214
216
  var count: int = deps.size()
215
217
  for dep: Dictionary in deps:
216
218
  if dep.has("dependencies"):
217
- count += _count_recursive(dep["dependencies"])
219
+ var deeper: Array[Dictionary] = dep["dependencies"]
220
+ count += _count_recursive(deeper)
218
221
  return count
219
222
 
220
223
 
@@ -243,8 +246,7 @@ func _analyze_resource(path: String, current_depth: int, walk: DependencyWalk) -
243
246
  file.close()
244
247
 
245
248
  for pattern: String in REFERENCE_PATTERNS:
246
- var regex: RegEx = RegEx.new()
247
- regex.compile(pattern)
249
+ var regex: RegEx = Patterns.compiled(pattern)
248
250
  for m: RegExMatch in regex.search_all(content):
249
251
  var dep_path: String = _referenced_path(m.get_string())
250
252
 
@@ -282,12 +284,13 @@ func _analyze_resource(path: String, current_depth: int, walk: DependencyWalk) -
282
284
 
283
285
  # The path inside a preload(), load() or ext_resource match; a bare res:// match is the path.
284
286
  func _referenced_path(matched: String) -> String:
285
- var inner: RegEx = RegEx.new()
287
+ var expression: String = ""
286
288
  if "preload" in matched or "load" in matched:
287
- inner.compile('"([^"]+)"')
289
+ expression = '"([^"]+)"'
288
290
  elif "ext_resource" in matched:
289
- inner.compile('path="([^"]+)"')
291
+ expression = 'path="([^"]+)"'
290
292
  else:
291
293
  return matched
294
+ var inner: RegEx = Patterns.compiled(expression)
292
295
  var inner_match: RegExMatch = inner.search(matched)
293
296
  return inner_match.get_string(1) if inner_match else matched
@@ -24,7 +24,8 @@ func find_files(path: String, extension: String) -> Array[String]:
24
24
  var dir: DirAccess = DirAccess.open(path)
25
25
 
26
26
  if dir:
27
- dir.list_dir_begin()
27
+ if dir.list_dir_begin() != OK:
28
+ return files
28
29
  var file_name: String = dir.get_next()
29
30
 
30
31
  while file_name != "":
@@ -47,7 +48,8 @@ func find_files_with_extensions(path: String, extensions: Array) -> Array[String
47
48
  var dir: DirAccess = DirAccess.open(path)
48
49
 
49
50
  if dir:
50
- dir.list_dir_begin()
51
+ if dir.list_dir_begin() != OK:
52
+ return files
51
53
  var file_name: String = dir.get_next()
52
54
 
53
55
  while file_name != "":
@@ -1,5 +1,6 @@
1
1
  extends RefCounted
2
2
 
3
+ const Patterns = preload("patterns.gd")
3
4
  const Log = preload("logger.gd")
4
5
 
5
6
  var _log: Log
@@ -278,9 +279,7 @@ func _parse_param(param_text: String) -> Dictionary:
278
279
 
279
280
  func _extract_dependencies(line: String) -> Array[String]:
280
281
  var deps: Array[String] = []
281
- var regex: RegEx = RegEx.new()
282
-
283
- regex.compile("(?:preload|load)\\s*\\(\\s*[\"']([^\"']+)[\"']\\s*\\)")
282
+ var regex: RegEx = Patterns.compiled("(?:preload|load)\\s*\\(\\s*[\"']([^\"']+)[\"']\\s*\\)")
284
283
 
285
284
  for m: RegExMatch in regex.search_all(line):
286
285
  deps.append(m.get_string(1))
@@ -1,5 +1,6 @@
1
1
  extends RefCounted
2
2
 
3
+ const Read = preload("reading.gd")
3
4
  const Log = preload("logger.gd")
4
5
 
5
6
  var _log: Log
@@ -58,8 +59,10 @@ func create_gdscript(params: Dictionary) -> Dictionary:
58
59
  if not file:
59
60
  return _log.failure("Failed to create script file: " + full_script_path)
60
61
 
61
- file.store_string(script_content)
62
+ var stored: bool = file.store_string(script_content)
62
63
  file.close()
64
+ if not stored:
65
+ return _log.failure("Failed to write to script file: " + full_script_path)
63
66
 
64
67
  # Whether the engine accepts what was written, parsed under this project's own warning
65
68
  # settings: a script that does not load is a script the caller wants to hear about now,
@@ -132,8 +135,10 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
132
135
  if not file:
133
136
  return _log.failure("Failed to write to script file: " + full_script_path)
134
137
 
135
- file.store_string("\n".join(lines))
138
+ var rewrote: bool = file.store_string("\n".join(lines))
136
139
  file.close()
140
+ if not rewrote:
141
+ return _log.failure("Failed to write to script file: " + full_script_path)
137
142
 
138
143
  return {
139
144
  "success": true,
@@ -143,14 +148,15 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
143
148
  }
144
149
 
145
150
 
146
- # Inserts the declaration and answers with its one-based line number.
151
+ # Inserts the declaration and answers with its one-based line number, or 0 for one that could not
152
+ # be placed, which is a number no line has.
147
153
  func _add_variable(lines: Array[String], mod: Dictionary) -> int:
148
154
  var var_name: String = str(mod.get("name", ""))
149
155
  var var_type: String = str(mod.get("varType", ""))
150
156
  var default_value: String = str(mod.get("defaultValue", ""))
151
- var is_export: bool = bool(mod.get("isExport", false))
157
+ var is_export: bool = Read.as_bool(mod.get("isExport", false))
152
158
  var export_hint: String = str(mod.get("exportHint", ""))
153
- var is_onready: bool = bool(mod.get("isOnready", false))
159
+ var is_onready: bool = Read.as_bool(mod.get("isOnready", false))
154
160
 
155
161
  var var_line: String = ""
156
162
 
@@ -179,7 +185,9 @@ func _add_variable(lines: Array[String], mod: Dictionary) -> int:
179
185
  var_line += ": Variant"
180
186
 
181
187
  var insert_line: int = _variable_insertion_point(lines)
182
- lines.insert(insert_line, var_line)
188
+ if lines.insert(insert_line, var_line) != OK:
189
+ push_error("gdharness: could not place a declaration at line " + str(insert_line))
190
+ return 0
183
191
  return insert_line + 1
184
192
 
185
193
 
@@ -208,7 +216,9 @@ func _add_signal(lines: Array[String], mod: Dictionary) -> int:
208
216
  signal_line += "(" + signal_params + ")"
209
217
 
210
218
  var insert_line: int = _signal_insertion_point(lines)
211
- lines.insert(insert_line, signal_line)
219
+ if lines.insert(insert_line, signal_line) != OK:
220
+ push_error("gdharness: could not place a signal at line " + str(insert_line))
221
+ return 0
212
222
  return insert_line + 1
213
223
 
214
224
 
@@ -234,7 +244,9 @@ func _add_function(lines: Array[String], mod: Dictionary) -> int:
234
244
  var insert_line: int = _function_insertion_point(lines, position)
235
245
 
236
246
  for i: int in range(func_lines.size() - 1, -1, -1):
237
- lines.insert(insert_line, func_lines[i])
247
+ if lines.insert(insert_line, func_lines[i]) != OK:
248
+ push_error("gdharness: could not place a function at line " + str(insert_line))
249
+ return 0
238
250
 
239
251
  return insert_line + 1
240
252
 
@@ -48,10 +48,15 @@ func _init() -> void:
48
48
  _log.debug("All arguments: " + str(args))
49
49
 
50
50
  var operation: String = args[script_index + 2]
51
- var params: Variant = _read_params(args[params_index])
52
- if not params is Dictionary:
51
+ var read: Variant = _read_params(args[params_index])
52
+ if not read is Dictionary:
53
53
  quit(1)
54
54
  return
55
+ # Through a typed local rather than straight into the call. The check above proves the type to
56
+ # a reader and not to the analyser, so the call handed a Variant to a Dictionary parameter, and
57
+ # a project holding unsafe_call_argument at error level then refused to compile this script at
58
+ # all. It is compiled under the target project's warning levels, not under this package's.
59
+ var params: Dictionary = read
55
60
 
56
61
  _log.info("Executing operation: " + operation)
57
62
 
@@ -1,5 +1,6 @@
1
1
  extends RefCounted
2
2
 
3
+ const Read = preload("reading.gd")
3
4
  const FileWalk = preload("file_walk.gd")
4
5
  const Log = preload("logger.gd")
5
6
 
@@ -18,7 +19,7 @@ func _init(p_log: Log) -> void:
18
19
  # Get import status for resources
19
20
  func get_import_status(params: Dictionary) -> Dictionary:
20
21
  var resource_path: String = str(params.get("resource_path", ""))
21
- var include_up_to_date: bool = bool(params.get("include_up_to_date", false))
22
+ var include_up_to_date: bool = Read.as_bool(params.get("include_up_to_date", false))
22
23
 
23
24
  _log.info(
24
25
  (
@@ -90,7 +91,7 @@ func set_import_options(params: Dictionary) -> Dictionary:
90
91
  resource_path = "res://" + resource_path
91
92
 
92
93
  var options: Dictionary = params.get("options", {})
93
- var do_reimport: bool = bool(params.get("reimport", true))
94
+ var do_reimport: bool = Read.as_bool(params.get("reimport", true), true)
94
95
 
95
96
  _log.info("Setting import options for: " + resource_path)
96
97
 
@@ -131,7 +132,7 @@ func set_import_options(params: Dictionary) -> Dictionary:
131
132
  # Reimport a resource or all resources
132
133
  func reimport_resource(params: Dictionary) -> Dictionary:
133
134
  var resource_path: String = str(params.get("resource_path", ""))
134
- var force: bool = bool(params.get("force", false))
135
+ var force: bool = Read.as_bool(params.get("force", false))
135
136
 
136
137
  _log.info(
137
138
  (
@@ -208,7 +209,7 @@ func list_export_presets(_params: Dictionary) -> Dictionary:
208
209
  # Validate project for export
209
210
  func validate_project(params: Dictionary) -> Dictionary:
210
211
  var preset_name: String = str(params.get("preset", ""))
211
- var include_suggestions: bool = bool(params.get("include_suggestions", true))
212
+ var include_suggestions: bool = Read.as_bool(params.get("include_suggestions", true), true)
212
213
 
213
214
  _log.info("Validating project" + (" for preset: " + preset_name if not preset_name.is_empty() else ""))
214
215
 
@@ -1,5 +1,6 @@
1
1
  extends RefCounted
2
2
 
3
+ const Read = preload("reading.gd")
3
4
  const Log = preload("logger.gd")
4
5
 
5
6
  # The key names a caller may write, and what the engine calls them.
@@ -106,7 +107,7 @@ func _init(p_log: Log) -> void:
106
107
  func add_input_action(params: Dictionary) -> Dictionary:
107
108
  var action_name: String = str(params.get("action_name", ""))
108
109
  var events: Array = params.get("events", [])
109
- var deadzone: float = float(params.get("deadzone", 0.5))
110
+ var deadzone: float = Read.as_float(params.get("deadzone", 0.5), 0.5)
110
111
 
111
112
  _log.info("Adding input action: " + action_name)
112
113
  _log.debug("Events: " + JSON.stringify(events))
@@ -200,26 +201,29 @@ func build_input_action(deadzone: float, events: Array) -> Dictionary:
200
201
  match evt_class:
201
202
  "InputEventKey":
202
203
  var key: InputEventKey = InputEventKey.new()
203
- key.keycode = int(event.get("keycode", 0)) as Key
204
- key.ctrl_pressed = bool(event.get("ctrl_pressed", false))
205
- key.alt_pressed = bool(event.get("alt_pressed", false))
206
- key.shift_pressed = bool(event.get("shift_pressed", false))
204
+ key.keycode = Read.as_int(event.get("keycode", 0)) as Key
205
+ key.ctrl_pressed = Read.as_bool(event.get("ctrl_pressed", false))
206
+ key.alt_pressed = Read.as_bool(event.get("alt_pressed", false))
207
+ key.shift_pressed = Read.as_bool(event.get("shift_pressed", false))
207
208
  built.append(key)
208
209
 
209
210
  "InputEventMouseButton":
210
211
  var mouse: InputEventMouseButton = InputEventMouseButton.new()
211
- mouse.button_index = (int(event.get("button_index", MOUSE_BUTTON_LEFT)) as MouseButton)
212
+ mouse.button_index = (
213
+ Read.as_int(event.get("button_index", MOUSE_BUTTON_LEFT), MOUSE_BUTTON_LEFT)
214
+ as MouseButton
215
+ )
212
216
  built.append(mouse)
213
217
 
214
218
  "InputEventJoypadButton":
215
219
  var pad: InputEventJoypadButton = InputEventJoypadButton.new()
216
- pad.button_index = int(event.get("button_index", 0)) as JoyButton
220
+ pad.button_index = Read.as_int(event.get("button_index", 0)) as JoyButton
217
221
  built.append(pad)
218
222
 
219
223
  "InputEventJoypadMotion":
220
224
  var motion: InputEventJoypadMotion = InputEventJoypadMotion.new()
221
- motion.axis = int(event.get("axis", 0)) as JoyAxis
222
- motion.axis_value = float(event.get("axis_value", 1.0))
225
+ motion.axis = Read.as_int(event.get("axis", 0)) as JoyAxis
226
+ motion.axis_value = Read.as_float(event.get("axis_value", 1.0), 1.0)
223
227
  built.append(motion)
224
228
 
225
229
  _:
@@ -0,0 +1,22 @@
1
+ extends RefCounted
2
+
3
+ # A compiled regular expression, with a pattern that will not compile said rather than dropped.
4
+ #
5
+ # `RegEx.compile()` answers with an Error, and every caller here passes a literal its author had
6
+ # already checked by eye, so the answer was thrown away eleven times. A project holding
7
+ # `return_value_discarded` at error level will not compile a script that does that, and these
8
+ # scripts are compiled under the target project's warning levels rather than under this package's:
9
+ # one project turning that warning on loses every headless operation at once.
10
+ #
11
+ # A pattern that fails here is a mistake in this repository rather than anything a caller did, so
12
+ # it goes to the engine's error stream, where the operation's answer carries it back under
13
+ # `engine_messages`. The empty RegEx it then returns matches nothing, which is the honest result of
14
+ # a pattern that does not exist.
15
+
16
+
17
+ static func compiled(expression: String) -> RegEx:
18
+ var regex: RegEx = RegEx.new()
19
+ var built: Error = regex.compile(expression)
20
+ if built != OK:
21
+ push_error("gdharness: could not compile the pattern " + expression)
22
+ return regex
@@ -1,5 +1,6 @@
1
1
  extends RefCounted
2
2
 
3
+ const Patterns = preload("patterns.gd")
3
4
  const Log = preload("logger.gd")
4
5
 
5
6
  var _log: Log
@@ -30,8 +31,7 @@ func list_plugins(_params: Dictionary) -> Dictionary:
30
31
  var enabled_count: int = 0
31
32
 
32
33
  var dir: DirAccess = DirAccess.open(addons_path)
33
- if dir:
34
- dir.list_dir_begin()
34
+ if dir and dir.list_dir_begin() == OK:
35
35
  var folder_name: String = dir.get_next()
36
36
 
37
37
  while folder_name != "":
@@ -122,8 +122,7 @@ func disable_plugin(params: Dictionary) -> Dictionary:
122
122
  # type; the String form is what an older hand-edited file can carry.
123
123
  func _enabled_plugin_names() -> Array[String]:
124
124
  var names: Array[String] = []
125
- var regex: RegEx = RegEx.new()
126
- regex.compile("res://addons/([^/]+)/plugin.cfg")
125
+ var regex: RegEx = Patterns.compiled("res://addons/([^/]+)/plugin.cfg")
127
126
 
128
127
  var enabled_value: Variant = ProjectSettings.get_setting("editor_plugins/enabled", PackedStringArray())
129
128
  if enabled_value is PackedStringArray:
@@ -151,9 +150,14 @@ func _save_enabled_plugins(names: Array[String]) -> Error:
151
150
  ProjectSettings.set_setting("editor_plugins/enabled", null)
152
151
  return ProjectSettings.save()
153
152
 
154
- var enabled_paths: PackedStringArray = PackedStringArray()
153
+ # Gathered in an Array[String] and converted at the end, because appending to a packed array
154
+ # answers with whether it worked and a project holding return_value_discarded at error level
155
+ # will not compile a script that drops that answer. The conversion is what matters: an
156
+ # Array[String] handed to set_setting is written as Array[String]([...]), which the editor does
157
+ # not read as a plugin list at all.
158
+ var enabled_paths: Array[String] = []
155
159
  for name: String in names:
156
160
  enabled_paths.append("res://addons/" + name + "/plugin.cfg")
157
161
 
158
- ProjectSettings.set_setting("editor_plugins/enabled", enabled_paths)
162
+ ProjectSettings.set_setting("editor_plugins/enabled", PackedStringArray(enabled_paths))
159
163
  return ProjectSettings.save()
@@ -1,5 +1,6 @@
1
1
  extends RefCounted
2
2
 
3
+ const Read = preload("reading.gd")
3
4
  const Log = preload("logger.gd")
4
5
  const Serialisation = preload("serialisation.gd")
5
6
 
@@ -11,9 +12,15 @@ func _init(p_log: Log) -> void:
11
12
  _log = p_log
12
13
 
13
14
 
14
- # Get a project setting value
15
+ # Get a project setting value, or every setting under a prefix.
15
16
  func get_project_setting(params: Dictionary) -> Dictionary:
17
+ var prefix: String = str(params.get("prefix", ""))
18
+ if not prefix.is_empty():
19
+ return _settings_under(prefix)
20
+
16
21
  var setting_path: String = str(params.get("setting", ""))
22
+ if setting_path.is_empty():
23
+ return _log.failure("setting is required, or prefix to read every setting under one")
17
24
 
18
25
  _log.info("Getting project setting: " + setting_path)
19
26
 
@@ -30,6 +37,37 @@ func get_project_setting(params: Dictionary) -> Dictionary:
30
37
  return result
31
38
 
32
39
 
40
+ # Every setting whose name starts with [prefix], with the type the engine registers for each.
41
+ #
42
+ # A caller after a whole family, "everything under debug/gdscript/warnings", had no way to ask: a get
43
+ # takes one name, and the engine is the only thing that knows what the family contains. The way round
44
+ # it was to walk ProjectSettings.get_property_list() from a script written for the purpose, which is
45
+ # an engine start to answer a question the engine was already open for.
46
+ #
47
+ # The type is here because it is the half a name hides. A setting sitting among a family of levels
48
+ # can be a bool, and a caller that assumes otherwise writes a level over it and sees nothing go
49
+ # wrong: 2 is as true as true is. Reading it out of the property list is how that is caught.
50
+ func _settings_under(prefix: String) -> Dictionary:
51
+ _log.info("Listing project settings under: " + prefix)
52
+
53
+ var found: Array[Dictionary] = []
54
+ for property: Dictionary in ProjectSettings.get_property_list():
55
+ var name: String = str(property.get("name", ""))
56
+ if not name.begins_with(prefix):
57
+ continue
58
+ var kind: int = Read.as_int(property.get("type", TYPE_NIL), TYPE_NIL)
59
+ var value: Variant = ProjectSettings.get_setting(name)
60
+ var described: Dictionary = {
61
+ "setting": name,
62
+ "type": type_string(kind),
63
+ "value": _values.serialize_value(value),
64
+ }
65
+ found.append(described)
66
+
67
+ found.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["setting"] < b["setting"])
68
+ return {"prefix": prefix, "count": found.size(), "settings": found}
69
+
70
+
33
71
  # Set a project setting value
34
72
  func set_project_setting(params: Dictionary) -> Dictionary:
35
73
  var setting_path: String = str(params.get("setting", ""))
@@ -85,7 +123,7 @@ func set_project_setting(params: Dictionary) -> Dictionary:
85
123
  func _wanted_type(setting_path: String, current: Variant) -> int:
86
124
  for info: Dictionary in ProjectSettings.get_property_list():
87
125
  if str(info.get("name", "")) == setting_path:
88
- return int(info.get("type", TYPE_NIL))
126
+ return Read.as_int(info.get("type", TYPE_NIL), TYPE_NIL)
89
127
  return typeof(current)
90
128
 
91
129
 
@@ -101,7 +139,7 @@ func _converts_faithfully(value: Variant, to: int) -> bool:
101
139
  func add_autoload(params: Dictionary) -> Dictionary:
102
140
  var name: String = str(params.get("name", ""))
103
141
  var path: String = str(params.get("path", ""))
104
- var enabled: bool = bool(params.get("enabled", true))
142
+ var enabled: bool = Read.as_bool(params.get("enabled", true), true)
105
143
 
106
144
  if not path.begins_with("res://"):
107
145
  path = "res://" + path
@@ -0,0 +1,66 @@
1
+ extends RefCounted
2
+
3
+ # A value out of a dictionary that came from JSON, as the type the operation wants.
4
+ #
5
+ # `int(value)`, `float(value)` and `bool(value)` take a Variant, and a project holding
6
+ # `unsafe_call_argument` at error level will not compile a script that hands one over. These
7
+ # scripts are compiled under the target project's warning levels rather than under this package's,
8
+ # because `--path` makes that project the loaded one and the file's living in the npm cache
9
+ # changes nothing: one project turning that warning on lost every headless operation at once.
10
+ #
11
+ # The conversion itself is the same. What changes is that the type is established first, on a
12
+ # typed local, and the conversion happens from that. Here rather than at each of the thirty-one
13
+ # places that read a parameter, because thirty-one is how many chances there are to write the old
14
+ # form again, and the error does not appear until somebody sets the warning.
15
+ #
16
+ # `fallback` answers a value that is there and is null, which is a caller sending JSON null where a
17
+ # number was expected. Absent keys never reach here: the callers pass their own default to
18
+ # `Dictionary.get`, so what arrives is that default.
19
+
20
+
21
+ static func as_int(value: Variant, fallback: int = 0) -> int:
22
+ if value is int:
23
+ var already: int = value
24
+ return already
25
+ if value is float:
26
+ var real: float = value
27
+ return int(real)
28
+ if value is bool:
29
+ var flag: bool = value
30
+ return 1 if flag else 0
31
+ if value is String:
32
+ var text: String = value
33
+ return text.to_int()
34
+ return fallback
35
+
36
+
37
+ static func as_float(value: Variant, fallback: float = 0.0) -> float:
38
+ if value is float:
39
+ var already: float = value
40
+ return already
41
+ if value is int:
42
+ var whole: int = value
43
+ return float(whole)
44
+ if value is bool:
45
+ var flag: bool = value
46
+ return 1.0 if flag else 0.0
47
+ if value is String:
48
+ var text: String = value
49
+ return text.to_float()
50
+ return fallback
51
+
52
+
53
+ static func as_bool(value: Variant, fallback: bool = false) -> bool:
54
+ if value is bool:
55
+ var already: bool = value
56
+ return already
57
+ if value is int:
58
+ var whole: int = value
59
+ return whole != 0
60
+ if value is float:
61
+ var real: float = value
62
+ return not is_zero_approx(real)
63
+ if value is String:
64
+ var text: String = value
65
+ return not text.is_empty()
66
+ return fallback
@@ -1,5 +1,7 @@
1
1
  extends RefCounted
2
2
 
3
+ const Read = preload("reading.gd")
4
+
3
5
  # What each Godot type becomes on the wire, as one table you can read rather than an order you
4
6
  # have to trust. Keyed on typeof() rather than written as a chain of `is` tests, because the
5
7
  # order mattered: Resource had to be tested before Object or every resource came back as a bare
@@ -60,26 +62,56 @@ func deserialize_value(value: Variant) -> Variant:
60
62
  rebuilt[key] = deserialize_value(fields[key])
61
63
  return rebuilt
62
64
 
65
+ # Each component read as the type the constructor wants before it is handed over. These come
66
+ # out of JSON as Variant, and a project holding `unsafe_call_argument` at error level refuses
67
+ # to compile a script that gives a Variant to a typed parameter: the operations are compiled
68
+ # under the target project's warning levels, so one such project lost every headless call.
63
69
  match fields["_type"]:
64
70
  "Vector2":
65
- return Vector2(fields.get("x", 0), fields.get("y", 0))
71
+ return Vector2(Read.as_float(fields.get("x", 0)), Read.as_float(fields.get("y", 0)))
66
72
  "Vector3":
67
- return Vector3(fields.get("x", 0), fields.get("y", 0), fields.get("z", 0))
73
+ return Vector3(
74
+ Read.as_float(fields.get("x", 0)),
75
+ Read.as_float(fields.get("y", 0)),
76
+ Read.as_float(fields.get("z", 0))
77
+ )
68
78
  "Vector2i":
69
- return Vector2i(fields.get("x", 0), fields.get("y", 0))
79
+ return Vector2i(Read.as_int(fields.get("x", 0)), Read.as_int(fields.get("y", 0)))
70
80
  "Vector3i":
71
- return Vector3i(fields.get("x", 0), fields.get("y", 0), fields.get("z", 0))
81
+ return Vector3i(
82
+ Read.as_int(fields.get("x", 0)),
83
+ Read.as_int(fields.get("y", 0)),
84
+ Read.as_int(fields.get("z", 0))
85
+ )
72
86
  "Color":
73
- return Color(fields.get("r", 0), fields.get("g", 0), fields.get("b", 0), fields.get("a", 1))
87
+ return Color(
88
+ Read.as_float(fields.get("r", 0)),
89
+ Read.as_float(fields.get("g", 0)),
90
+ Read.as_float(fields.get("b", 0)),
91
+ Read.as_float(fields.get("a", 1), 1.0)
92
+ )
74
93
  "Rect2":
75
- var position: Variant = deserialize_value(fields.get("position", {}))
76
- var size: Variant = deserialize_value(fields.get("size", {}))
77
- return Rect2(position, size)
94
+ return _rect_from(fields)
78
95
  "NodePath":
79
- return NodePath(fields.get("path", ""))
96
+ return NodePath(str(fields.get("path", "")))
80
97
  return fields
81
98
 
82
99
 
100
+ ## A Rect2 out of its two tagged corners, or the fields back when they do not describe one.
101
+ ##
102
+ ## Its own function because the components have to be read as Vector2 before the constructor is
103
+ ## given them, and a branch that can also answer with the fields would push the match above past
104
+ ## the return count this project holds itself to.
105
+ func _rect_from(fields: Dictionary) -> Variant:
106
+ var position: Variant = deserialize_value(fields.get("position", {}))
107
+ var size: Variant = deserialize_value(fields.get("size", {}))
108
+ if not (position is Vector2 and size is Vector2):
109
+ return fields
110
+ var at: Vector2 = position
111
+ var extent: Vector2 = size
112
+ return Rect2(at, extent)
113
+
114
+
83
115
  func _serialize_nil(_value: Variant) -> Variant:
84
116
  return null
85
117