gdharness 1.3.0 → 1.3.2

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 (72) hide show
  1. package/build/cli.js +322 -96
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -19
  3. package/build/godot/addons/gdharness_editor/bridge_client.gd +8 -8
  4. package/build/godot/addons/gdharness_editor/declared_values.gd +116 -0
  5. package/build/godot/addons/gdharness_editor/declared_values.gd.uid +1 -0
  6. package/build/godot/addons/gdharness_editor/file_reads.gd +65 -25
  7. package/build/godot/addons/gdharness_editor/headless_check.gd +7 -7
  8. package/build/godot/addons/gdharness_editor/plugin.gd +6 -6
  9. package/build/godot/addons/gdharness_editor/property_values.gd +222 -59
  10. package/build/godot/addons/gdharness_editor/reading.gd +65 -0
  11. package/build/godot/addons/gdharness_editor/scene_file.gd +28 -23
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +49 -94
  13. package/build/godot/addons/gdharness_editor/tool_executor.gd +24 -24
  14. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +52 -41
  15. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +4 -4
  16. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +2 -2
  17. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +14 -14
  18. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +175 -74
  19. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +17 -17
  20. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +14 -12
  21. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +7 -7
  22. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +88 -84
  23. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +4 -4
  24. package/build/godot/addons/gdharness_editor/value_shapes.gd +118 -0
  25. package/build/godot/addons/gdharness_editor/value_shapes.gd.uid +1 -0
  26. package/build/godot/addons/gdharness_runtime/reading.gd +65 -0
  27. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +18 -18
  28. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +29 -29
  29. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +21 -18
  30. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +86 -80
  31. package/build/godot/addons/gdharness_runtime/runtime_input.gd +65 -64
  32. package/build/godot/addons/gdharness_runtime/runtime_items.gd +6 -6
  33. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +8 -8
  34. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +6 -6
  35. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +10 -10
  36. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +7 -7
  37. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +41 -41
  38. package/build/godot/addons/gdharness_runtime/runtime_says.gd +3 -3
  39. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +33 -33
  40. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +20 -20
  41. package/build/godot/addons/gdharness_runtime/runtime_values.gd +52 -25
  42. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +56 -50
  43. package/build/godot/addons/gdharness_runtime/serialisation.gd +49 -94
  44. package/build/godot/addons/gdharness_runtime/value_shapes.gd +118 -0
  45. package/build/godot/addons/gdharness_runtime/value_shapes.gd.uid +1 -0
  46. package/build/godot/operations/animation_reads.gd +7 -19
  47. package/build/godot/operations/audio_buses.gd +10 -10
  48. package/build/godot/operations/class_cache.gd +16 -16
  49. package/build/godot/operations/classdb_queries.gd +25 -15
  50. package/build/godot/operations/declared_values.gd +116 -0
  51. package/build/godot/operations/dependencies.gd +141 -33
  52. package/build/godot/operations/editor_tools.gd +12 -12
  53. package/build/godot/operations/export_presets.gd +14 -14
  54. package/build/godot/operations/file_reads.gd +65 -25
  55. package/build/godot/operations/file_walk.gd +34 -0
  56. package/build/godot/operations/gdscript_analysis.gd +27 -22
  57. package/build/godot/operations/gdscript_authoring.gd +122 -17
  58. package/build/godot/operations/godot_operations.gd +66 -63
  59. package/build/godot/operations/import_pipeline.gd +59 -20
  60. package/build/godot/operations/input_actions.gd +90 -18
  61. package/build/godot/operations/logger.gd +1 -1
  62. package/build/godot/operations/plugins.gd +8 -8
  63. package/build/godot/operations/project_config.gd +83 -20
  64. package/build/godot/operations/project_diagnostics.gd +36 -11
  65. package/build/godot/operations/property_values.gd +222 -59
  66. package/build/godot/operations/reading.gd +65 -0
  67. package/build/godot/operations/rename_checks.gd +3 -3
  68. package/build/godot/operations/resource_files.gd +9 -6
  69. package/build/godot/operations/script_check.gd +23 -7
  70. package/build/godot/operations/serialisation.gd +49 -94
  71. package/build/godot/operations/value_shapes.gd +118 -0
  72. package/package.json +1 -1
@@ -14,25 +14,25 @@ extends SceneTree
14
14
  # the operations directory ships inside the server package and is handed to the engine as an
15
15
  # absolute path outside the project. A res:// path would only resolve in the test fixture.
16
16
 
17
- const AnimationReads = preload("animation_reads.gd")
18
- const AudioBuses = preload("audio_buses.gd")
19
- const ClassCache = preload("class_cache.gd")
20
- const ClassDbQueries = preload("classdb_queries.gd")
21
- const Dependencies = preload("dependencies.gd")
22
- const ExportPresets = preload("export_presets.gd")
23
- const FileReads = preload("file_reads.gd")
24
- const GdscriptAnalysis = preload("gdscript_analysis.gd")
25
- const GdscriptAuthoring = preload("gdscript_authoring.gd")
26
- const ImportPipeline = preload("import_pipeline.gd")
27
- const InputActions = preload("input_actions.gd")
28
- const Log = preload("logger.gd")
29
- const Plugins = preload("plugins.gd")
30
- const ProjectConfig = preload("project_config.gd")
31
- const ProjectDiagnostics = preload("project_diagnostics.gd")
32
- const Read = preload("reading.gd")
33
- const RenameChecks = preload("rename_checks.gd")
34
- const ResourceFiles = preload("resource_files.gd")
35
- const ScriptCheck = preload("script_check.gd")
17
+ const GdhAnimationReads = preload("animation_reads.gd")
18
+ const GdhAudioBuses = preload("audio_buses.gd")
19
+ const GdhClassCache = preload("class_cache.gd")
20
+ const GdhClassDbQueries = preload("classdb_queries.gd")
21
+ const GdhDependencies = preload("dependencies.gd")
22
+ const GdhExportPresets = preload("export_presets.gd")
23
+ const GdhFileReads = preload("file_reads.gd")
24
+ const GdhGdscriptAnalysis = preload("gdscript_analysis.gd")
25
+ const GdhGdscriptAuthoring = preload("gdscript_authoring.gd")
26
+ const GdhImportPipeline = preload("import_pipeline.gd")
27
+ const GdhInputActions = preload("input_actions.gd")
28
+ const GdhLog = preload("logger.gd")
29
+ const GdhPlugins = preload("plugins.gd")
30
+ const GdhProjectConfig = preload("project_config.gd")
31
+ const GdhProjectDiagnostics = preload("project_diagnostics.gd")
32
+ const GdhRead = preload("reading.gd")
33
+ const GdhRenameChecks = preload("rename_checks.gd")
34
+ const GdhResourceFiles = preload("resource_files.gd")
35
+ const GdhScriptCheck = preload("script_check.gd")
36
36
 
37
37
  ## Printed once the answer is written, after which the engine runs the project's autoloads. Kept in
38
38
  ## step with `ANSWERED` in src/headless.ts.
@@ -59,12 +59,12 @@ const AFTER_THE_AUTOLOADS: Array[String] = [
59
59
  "run_editor_tool",
60
60
  ]
61
61
 
62
- var _log: Log
62
+ var _log: GdhLog
63
63
 
64
64
 
65
65
  func _init() -> void:
66
66
  var args: PackedStringArray = OS.get_cmdline_args()
67
- _log = Log.new("--debug-godot" in args)
67
+ _log = GdhLog.new("--debug-godot" in args)
68
68
 
69
69
  # The script path is the argument after --script, so the operation and its parameters are
70
70
  # the two that follow it.
@@ -180,26 +180,27 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
180
180
  match operation:
181
181
  # Resource files
182
182
  "get_uid":
183
- payload = ResourceFiles.new(_log).get_uid(params)
183
+ payload = GdhResourceFiles.new(_log).get_uid(params)
184
184
  "refresh_class_cache":
185
- payload = ClassCache.new(_log).refresh_class_cache(params)
185
+ payload = GdhClassCache.new(_log).refresh_class_cache(params)
186
186
  "read_scene_tree":
187
187
  payload = _read(
188
- FileReads.scene_tree(
188
+ GdhFileReads.scene_tree(
189
189
  str(params.get("scene_path", "")),
190
- Read.as_int(params.get("depth", -1), -1),
191
- Read.as_bool(params.get("include_properties", false))
190
+ GdhRead.as_int(params.get("depth", -1), -1),
191
+ GdhRead.as_bool(params.get("include_properties", false))
192
192
  )
193
193
  )
194
194
  "read_resource":
195
195
  payload = _read(
196
- FileReads.resource(
197
- str(params.get("resource_path", "")), Read.as_bool(params.get("include_defaults", false))
196
+ GdhFileReads.resource(
197
+ str(params.get("resource_path", "")),
198
+ GdhRead.as_bool(params.get("include_defaults", false))
198
199
  )
199
200
  )
200
201
  "read_animations":
201
202
  payload = _read(
202
- AnimationReads.scene_animations(
203
+ GdhAnimationReads.scene_animations(
203
204
  str(params.get("scene_path", "")),
204
205
  str(params.get("player_node_path", ".")),
205
206
  str(params.get("animation_name", ""))
@@ -207,7 +208,7 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
207
208
  )
208
209
  "read_animation_states":
209
210
  payload = _read(
210
- AnimationReads.scene_states(
211
+ GdhAnimationReads.scene_states(
211
212
  str(params.get("scene_path", "")),
212
213
  str(params.get("anim_tree_path", "")),
213
214
  str(params.get("state_machine_path", ""))
@@ -219,83 +220,85 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
219
220
 
220
221
  # Import and export pipeline
221
222
  "get_import_status":
222
- payload = ImportPipeline.new(_log).get_import_status(params)
223
+ payload = GdhImportPipeline.new(_log).get_import_status(params)
223
224
  "get_import_options":
224
- payload = ImportPipeline.new(_log).get_import_options(params)
225
+ payload = GdhImportPipeline.new(_log).get_import_options(params)
225
226
  "set_import_options":
226
- payload = ImportPipeline.new(_log).set_import_options(params)
227
+ payload = GdhImportPipeline.new(_log).set_import_options(params)
227
228
  "list_export_presets":
228
- payload = ExportPresets.new(_log).list_export_presets(params)
229
+ payload = GdhExportPresets.new(_log).list_export_presets(params)
229
230
  "add_export_preset":
230
- payload = ExportPresets.new(_log).add_export_preset(params)
231
+ payload = GdhExportPresets.new(_log).add_export_preset(params)
231
232
  "validate_project":
232
- payload = ImportPipeline.new(_log).validate_project(params)
233
+ payload = GdhImportPipeline.new(_log).validate_project(params)
233
234
 
234
235
  # Dependencies and project inspection
235
236
  "get_dependencies":
236
- payload = Dependencies.new(_log).get_dependencies(params)
237
+ payload = GdhDependencies.new(_log).get_dependencies(params)
237
238
  "find_resource_usages":
238
- payload = Dependencies.new(_log).find_resource_usages(params)
239
+ payload = GdhDependencies.new(_log).find_resource_usages(params)
239
240
  "get_project_health":
240
- payload = ProjectDiagnostics.new(_log).get_project_health(params)
241
+ payload = GdhProjectDiagnostics.new(_log).get_project_health(params)
241
242
 
242
243
  # project.godot
243
244
  "get_project_setting":
244
- payload = ProjectConfig.new(_log).get_project_setting(params)
245
+ payload = GdhProjectConfig.new(_log).get_project_setting(params)
245
246
  "set_project_setting":
246
- payload = ProjectConfig.new(_log).set_project_setting(params)
247
+ payload = GdhProjectConfig.new(_log).set_project_setting(params)
247
248
  "add_autoload":
248
- payload = ProjectConfig.new(_log).add_autoload(params)
249
+ payload = GdhProjectConfig.new(_log).add_autoload(params)
249
250
  "remove_autoload":
250
- payload = ProjectConfig.new(_log).remove_autoload(params)
251
+ payload = GdhProjectConfig.new(_log).remove_autoload(params)
251
252
  "list_autoloads":
252
- payload = ProjectConfig.new(_log).list_autoloads(params)
253
+ payload = GdhProjectConfig.new(_log).list_autoloads(params)
253
254
  "set_main_scene":
254
- payload = ProjectConfig.new(_log).set_main_scene(params)
255
+ payload = GdhProjectConfig.new(_log).set_main_scene(params)
255
256
 
256
257
  # GDScript files
257
258
  "create_script":
258
- payload = GdscriptAuthoring.new(_log).create_gdscript(params)
259
+ payload = GdhGdscriptAuthoring.new(_log).create_gdscript(params)
259
260
  "modify_script":
260
- payload = GdscriptAuthoring.new(_log).modify_gdscript(params)
261
+ payload = GdhGdscriptAuthoring.new(_log).modify_gdscript(params)
261
262
  "get_script_info":
262
- payload = GdscriptAnalysis.new(_log).get_gdscript_info(params)
263
+ payload = GdhGdscriptAnalysis.new(_log).get_gdscript_info(params)
263
264
  "names_taken":
264
- payload = RenameChecks.new(_log).names_taken(params)
265
+ payload = GdhRenameChecks.new(_log).names_taken(params)
265
266
  "check_scripts":
266
- payload = ScriptCheck.new(_log).check_scripts(params)
267
+ payload = GdhScriptCheck.new(_log).check_scripts(params)
267
268
 
268
269
  # Plugins and input
269
270
  "list_plugins":
270
- payload = Plugins.new(_log).list_plugins(params)
271
+ payload = GdhPlugins.new(_log).list_plugins(params)
271
272
  "enable_plugin":
272
- payload = Plugins.new(_log).enable_plugin(params)
273
+ payload = GdhPlugins.new(_log).enable_plugin(params)
273
274
  "disable_plugin":
274
- payload = Plugins.new(_log).disable_plugin(params)
275
+ payload = GdhPlugins.new(_log).disable_plugin(params)
275
276
  "add_input_action":
276
- payload = InputActions.new(_log).add_input_action(params)
277
+ payload = GdhInputActions.new(_log).add_input_action(params)
277
278
  "remove_input_action":
278
- payload = InputActions.new(_log).remove_input_action(params)
279
+ payload = GdhInputActions.new(_log).remove_input_action(params)
280
+ "list_input_actions":
281
+ payload = GdhInputActions.new(_log).list_input_actions(params)
279
282
 
280
283
  # Audio
281
284
  "create_audio_bus":
282
- payload = AudioBuses.new(_log).create_audio_bus(params)
285
+ payload = GdhAudioBuses.new(_log).create_audio_bus(params)
283
286
  "remove_audio_bus":
284
- payload = AudioBuses.new(_log).remove_audio_bus(params)
287
+ payload = GdhAudioBuses.new(_log).remove_audio_bus(params)
285
288
  "get_audio_buses":
286
- payload = AudioBuses.new(_log).get_audio_buses(params)
289
+ payload = GdhAudioBuses.new(_log).get_audio_buses(params)
287
290
  "set_audio_bus_effect":
288
- payload = AudioBuses.new(_log).set_audio_bus_effect(params)
291
+ payload = GdhAudioBuses.new(_log).set_audio_bus_effect(params)
289
292
  "set_audio_bus_volume":
290
- payload = AudioBuses.new(_log).set_audio_bus_volume(params)
293
+ payload = GdhAudioBuses.new(_log).set_audio_bus_volume(params)
291
294
 
292
295
  # ClassDB
293
296
  "query_classes":
294
- payload = ClassDbQueries.new(_log).query_classes(params)
297
+ payload = GdhClassDbQueries.new(_log).query_classes(params)
295
298
  "query_class_info":
296
- payload = ClassDbQueries.new(_log).query_class_info(params)
299
+ payload = GdhClassDbQueries.new(_log).query_class_info(params)
297
300
  "inspect_inheritance":
298
- payload = ClassDbQueries.new(_log).inspect_inheritance(params)
301
+ payload = GdhClassDbQueries.new(_log).inspect_inheritance(params)
299
302
 
300
303
  _:
301
304
  _log.error("Unknown operation: " + operation)
@@ -1,10 +1,12 @@
1
1
  extends RefCounted
2
2
 
3
- const ExportPresets = preload("export_presets.gd")
4
- const Read = preload("reading.gd")
5
- const Serialisation = preload("serialisation.gd")
6
- const FileWalk = preload("file_walk.gd")
7
- const Log = preload("logger.gd")
3
+ const GdhDependencies = preload("dependencies.gd")
4
+ const GdhExportPresets = preload("export_presets.gd")
5
+ const GdhScriptCheck = preload("script_check.gd")
6
+ const GdhRead = preload("reading.gd")
7
+ const GdhSerialisation = preload("serialisation.gd")
8
+ const GdhFileWalk = preload("file_walk.gd")
9
+ const GdhLog = preload("logger.gd")
8
10
 
9
11
  ## What the engine imports with no tool outside it, for finding what has not been imported yet;
10
12
  ## everything imported already is found by its sidecar whatever its extension.
@@ -53,20 +55,20 @@ const OPENTYPE_OPTIONS: Dictionary[String, Array] = {"font_data_dynamic": ["open
53
55
  const EDITOR_DIRECTORY: String = "res://.godot/editor"
54
56
  const EDITOR_CACHE_PREFIX: String = "filesystem_cache"
55
57
 
56
- var _log: Log
57
- var _files: FileWalk = FileWalk.new()
58
- var _values: Serialisation = Serialisation.new()
58
+ var _log: GdhLog
59
+ var _files: GdhFileWalk = GdhFileWalk.new()
60
+ var _values: GdhSerialisation = GdhSerialisation.new()
59
61
  var _sidecar_md5s: Variant = null
60
62
 
61
63
 
62
- func _init(p_log: Log) -> void:
64
+ func _init(p_log: GdhLog) -> void:
63
65
  _log = p_log
64
66
 
65
67
 
66
68
  # Get import status for resources
67
69
  func get_import_status(params: Dictionary) -> Dictionary:
68
70
  var resource_path: String = str(params.get("resource_path", ""))
69
- var include_up_to_date: bool = Read.as_bool(params.get("include_up_to_date", false))
71
+ var include_up_to_date: bool = GdhRead.as_bool(params.get("include_up_to_date", false))
70
72
 
71
73
  _log.info(
72
74
  (
@@ -85,6 +87,14 @@ func get_import_status(params: Dictionary) -> Dictionary:
85
87
  if not full_path.begins_with("res://"):
86
88
  full_path = "res://" + full_path
87
89
 
90
+ # missing_source is a sidecar whose source has gone; with neither, the path names nothing the
91
+ # engine ever imported, and answering it as a lost source sent a caller to restore a file.
92
+ if not FileAccess.file_exists(full_path) and not FileAccess.file_exists(full_path + ".import"):
93
+ return _log.failure(
94
+ _files.with_nearest_files(
95
+ "Nothing is at %s, and no import sidecar names it" % full_path, full_path
96
+ )
97
+ )
88
98
  var status: Dictionary = _import_status_of(full_path, full_path + ".import")
89
99
  resources.append(status)
90
100
  _tally(summary, status)
@@ -203,7 +213,7 @@ func set_import_options(params: Dictionary) -> Dictionary:
203
213
  and (value is int or value is float)
204
214
  and type_convert(value, typeof(given)) != given
205
215
  )
206
- if held != null and (lossy or not Serialisation.acceptable(value, typeof(held))):
216
+ if held != null and (lossy or not GdhSerialisation.acceptable(value, typeof(held))):
207
217
  var held_type: String = type_string(typeof(held))
208
218
  var article: String = "an" if held_type.to_lower()[0] in "aeiou" else "a"
209
219
  refused.append(
@@ -323,7 +333,7 @@ static func _all_ints(held: Variant, keys: bool) -> bool:
323
333
  # Validate project for export
324
334
  func validate_project(params: Dictionary) -> Dictionary:
325
335
  var preset_name: String = str(params.get("preset", ""))
326
- var include_suggestions: bool = Read.as_bool(params.get("include_suggestions", true), true)
336
+ var include_suggestions: bool = GdhRead.as_bool(params.get("include_suggestions", true), true)
327
337
 
328
338
  _log.info("Validating project" + (" for preset: " + preset_name if not preset_name.is_empty() else ""))
329
339
 
@@ -355,12 +365,12 @@ func validate_project(params: Dictionary) -> Dictionary:
355
365
  include_suggestions
356
366
  )
357
367
  )
358
- elif not FileWalk.missing_reason(main_scene).is_empty():
368
+ elif not GdhFileWalk.missing_reason(main_scene).is_empty():
359
369
  issues.append(
360
370
  _finding(
361
371
  "error",
362
372
  "main_scene",
363
- "Main scene " + FileWalk.missing_reason(main_scene),
373
+ "Main scene " + GdhFileWalk.missing_reason(main_scene),
364
374
  "Update the main scene setting or create the missing scene file",
365
375
  include_suggestions
366
376
  )
@@ -382,7 +392,7 @@ func validate_project(params: Dictionary) -> Dictionary:
382
392
  # does not exist answered valid, as though it had been checked.
383
393
  if not preset_name.is_empty():
384
394
  checks_performed.append("export_preset")
385
- var named: Array[String] = ExportPresets.names()
395
+ var named: Array[String] = GdhExportPresets.names()
386
396
  if preset_name not in named:
387
397
  issues.append(
388
398
  _finding(
@@ -457,6 +467,35 @@ func validate_project(params: Dictionary) -> Dictionary:
457
467
  if "pass # TODO" in content:
458
468
  script_issues.append({"path": script_path, "issue": "Contains unimplemented functions"})
459
469
 
470
+ # Compiled by the server and handed in, as for the health check: a script the engine refuses
471
+ # stops the export's game at the first scene using it, which the comment check above never saw.
472
+ var compiled: Dictionary = params.get("compiled", {})
473
+ if compiled.has("checked"):
474
+ checks_performed.append("compiles")
475
+ var failures: Array = compiled.get("failures", [])
476
+ for failure: Dictionary in failures:
477
+ issues.append(
478
+ _finding(
479
+ "error",
480
+ "compiles",
481
+ GdhScriptCheck.failure_line(failure),
482
+ "script_diagnostics gives every error in it with its line",
483
+ include_suggestions
484
+ )
485
+ )
486
+
487
+ checks_performed.append("references")
488
+ for entry: Dictionary in GdhDependencies.new(_log).missing_references():
489
+ issues.append(
490
+ _finding(
491
+ "error",
492
+ "references",
493
+ GdhDependencies.missing_line(entry),
494
+ "Restore the file, or point the reference at the file that replaced it",
495
+ include_suggestions
496
+ )
497
+ )
498
+
460
499
  if script_issues.size() > 0:
461
500
  var warning: Dictionary = _finding(
462
501
  "warning",
@@ -611,8 +650,8 @@ static func _import_staleness(resource_path: String, sidecar: ConfigFile) -> Dic
611
650
  # imported them again: every scene after an engine upgrade raised the importer's version, and
612
651
  # every VRAM texture once the project asked for another compression format.
613
652
  var importer: String = str(sidecar.get_value("remap", "importer", ""))
614
- var version: int = Read.as_int(sidecar.get_value("remap", "importer_version", 0))
615
- var current: int = Read.as_int(IMPORTER_VERSIONS.get(importer, 0))
653
+ var version: int = GdhRead.as_int(sidecar.get_value("remap", "importer_version", 0))
654
+ var current: int = GdhRead.as_int(IMPORTER_VERSIONS.get(importer, 0))
616
655
  if current > version:
617
656
  return _stale(
618
657
  (
@@ -697,12 +736,12 @@ static func _vram_formats_lacking(metadata: Variant) -> Array[String]:
697
736
  if not metadata is Dictionary:
698
737
  return lacking
699
738
  var meta: Dictionary = metadata
700
- if not Read.as_bool(meta.get("vram_texture", false)):
739
+ if not GdhRead.as_bool(meta.get("vram_texture", false)):
701
740
  return lacking
702
741
  var written: Variant = meta.get("imported_formats", [])
703
742
  var formats: Array = written if written is Array else []
704
743
  for format: String in VRAM_FORMATS:
705
- var asked: bool = Read.as_bool(
744
+ var asked: bool = GdhRead.as_bool(
706
745
  ProjectSettings.get_setting("rendering/textures/vram_compression/import_" + format, false)
707
746
  )
708
747
  if asked and format not in formats:
@@ -850,7 +889,7 @@ static func _images_of(scene_path: String) -> Array[String]:
850
889
  text = bytes.slice(20, mini(20 + length, bytes.size())).get_string_from_utf8()
851
890
  if text.is_empty():
852
891
  return images
853
- var parsed: Variant = Read.json_or_null(text)
892
+ var parsed: Variant = GdhRead.json_or_null(text)
854
893
  if not parsed is Dictionary:
855
894
  return images
856
895
  var document: Dictionary = parsed
@@ -1,8 +1,8 @@
1
1
  extends RefCounted
2
2
 
3
- const Read = preload("reading.gd")
4
- const Log = preload("logger.gd")
5
- const ProjectFile = preload("project_file.gd")
3
+ const GdhRead = preload("reading.gd")
4
+ const GdhLog = preload("logger.gd")
5
+ const GdhProjectFile = preload("project_file.gd")
6
6
 
7
7
  # The key names a caller may write, and what the engine calls them.
8
8
  const KEY_NAMES: Dictionary = {
@@ -97,10 +97,10 @@ const KEY_NAMES: Dictionary = {
97
97
  "Equal": KEY_EQUAL,
98
98
  }
99
99
 
100
- var _log: Log
100
+ var _log: GdhLog
101
101
 
102
102
 
103
- func _init(p_log: Log) -> void:
103
+ func _init(p_log: GdhLog) -> void:
104
104
  _log = p_log
105
105
 
106
106
 
@@ -108,9 +108,27 @@ func _init(p_log: Log) -> void:
108
108
  func add_input_action(params: Dictionary) -> Dictionary:
109
109
  var action_name: String = str(params.get("action_name", ""))
110
110
  var events: Array = params.get("events", [])
111
- var deadzone: float = Read.as_float(params.get("deadzone", 0.5), 0.5)
111
+ var deadzone: float = GdhRead.as_float(params.get("deadzone", 0.5), 0.5)
112
112
 
113
113
  _log.info("Adding input action: " + action_name)
114
+ # The editor's Input Map refuses these, since project.godot keys an action by its name and
115
+ # the characters below are its own syntax there; a name holding one is written escaped and
116
+ # reads back as a name nobody typed.
117
+ for index: int in action_name.length():
118
+ var code: int = action_name.unicode_at(index)
119
+ if code < 32 or action_name[index] in ["/", ":", "=", "\\", '"']:
120
+ return (
121
+ _log
122
+ . failure(
123
+ (
124
+ (
125
+ "%s is not an action name the engine's Input Map takes: a name cannot hold / : = \\ "
126
+ + "or a double quote, or a control character. jump, move_left or ui_menu are names it takes."
127
+ )
128
+ % JSON.stringify(action_name)
129
+ )
130
+ )
131
+ )
114
132
  _log.debug("Events: " + JSON.stringify(events))
115
133
  _log.debug("Deadzone: " + str(deadzone))
116
134
 
@@ -130,7 +148,7 @@ func add_input_action(params: Dictionary) -> Dictionary:
130
148
  return _log.failure("A key event needs keycode")
131
149
  var key_value: int = _keycode_value(keycode)
132
150
  if key_value == KEY_NONE:
133
- return _log.failure("Unknown key: " + keycode)
151
+ return _log.failure(_unknown_key(keycode))
134
152
 
135
153
  var event_config: Dictionary = {"class_name": "InputEventKey", "keycode": key_value}
136
154
  if fields.get("ctrl", false):
@@ -192,12 +210,12 @@ func add_input_action(params: Dictionary) -> Dictionary:
192
210
  var bound: InputEvent = event
193
211
  old_events.append(_shaped(bound))
194
212
  answer["old_events"] = old_events
195
- answer["old_deadzone"] = Read.as_float(old.get("deadzone", 0.5), 0.5)
213
+ answer["old_deadzone"] = GdhRead.as_float(old.get("deadzone", 0.5), 0.5)
196
214
 
197
215
  # Through ProjectSettings so the file is saved the way the editor saves it, header and
198
216
  # every other line kept; a ConfigFile of project.godot drops the comments on the way out.
199
217
  ProjectSettings.set_setting(setting, action)
200
- var err: Error = ProjectFile.save()
218
+ var err: Error = GdhProjectFile.save()
201
219
  if err != OK:
202
220
  return _log.failure("Failed to save project.godot: " + error_string(err))
203
221
 
@@ -229,6 +247,42 @@ static func _shaped(event: InputEvent) -> Dictionary:
229
247
  return shaped
230
248
 
231
249
 
250
+ ## The input actions project.godot holds, each with its deadzone and events in the form
251
+ ## add_input_action answers with. An engine ui_ action is listed only when the project rebinds it,
252
+ ## as engineAction, since the engine's own eighty are there in every project.
253
+ func list_input_actions(_params: Dictionary) -> Dictionary:
254
+ var actions: Array[Dictionary] = []
255
+ for info: Dictionary in ProjectSettings.get_property_list():
256
+ var setting: String = str(info.get("name", ""))
257
+ if not setting.begins_with("input/"):
258
+ continue
259
+ var engine_default: Variant = ProjectSettings.property_get_revert(setting)
260
+ var held: Variant = ProjectSettings.get_setting(setting)
261
+ if engine_default != null and held == engine_default:
262
+ continue
263
+ var action: Dictionary = held if held is Dictionary else {}
264
+ var events: Array[Dictionary] = []
265
+ var listed: Variant = action.get("events", [])
266
+ if listed is Array:
267
+ var bound: Array = listed
268
+ for event: Variant in bound:
269
+ if event is InputEvent:
270
+ var shaped: InputEvent = event
271
+ events.append(_shaped(shaped))
272
+ (
273
+ actions
274
+ . append(
275
+ {
276
+ "name": setting.trim_prefix("input/"),
277
+ "deadzone": GdhRead.as_float(action.get("deadzone", 0.5), 0.5),
278
+ "events": events,
279
+ "engineAction": engine_default != null,
280
+ }
281
+ )
282
+ )
283
+ return {"actions": actions, "count": actions.size()}
284
+
285
+
232
286
  # Remove an input action the project defines, which add_input_action had no way back from.
233
287
  func remove_input_action(params: Dictionary) -> Dictionary:
234
288
  var action_name: String = str(params.get("action_name", ""))
@@ -250,7 +304,7 @@ func remove_input_action(params: Dictionary) -> Dictionary:
250
304
  )
251
305
  )
252
306
  ProjectSettings.set_setting(setting, null)
253
- var err: Error = ProjectFile.save()
307
+ var err: Error = GdhProjectFile.save()
254
308
  if err != OK:
255
309
  return _log.failure("Failed to save project.godot: " + error_string(err))
256
310
  return {"action_name": action_name, "removed": true}
@@ -273,29 +327,29 @@ func build_input_action(deadzone: float, events: Array) -> Dictionary:
273
327
  match evt_class:
274
328
  "InputEventKey":
275
329
  var key: InputEventKey = InputEventKey.new()
276
- key.keycode = Read.as_int(event.get("keycode", 0)) as Key
277
- key.ctrl_pressed = Read.as_bool(event.get("ctrl_pressed", false))
278
- key.alt_pressed = Read.as_bool(event.get("alt_pressed", false))
279
- key.shift_pressed = Read.as_bool(event.get("shift_pressed", false))
330
+ key.keycode = GdhRead.as_int(event.get("keycode", 0)) as Key
331
+ key.ctrl_pressed = GdhRead.as_bool(event.get("ctrl_pressed", false))
332
+ key.alt_pressed = GdhRead.as_bool(event.get("alt_pressed", false))
333
+ key.shift_pressed = GdhRead.as_bool(event.get("shift_pressed", false))
280
334
  built.append(key)
281
335
 
282
336
  "InputEventMouseButton":
283
337
  var mouse: InputEventMouseButton = InputEventMouseButton.new()
284
338
  mouse.button_index = (
285
- Read.as_int(event.get("button_index", MOUSE_BUTTON_LEFT), MOUSE_BUTTON_LEFT)
339
+ GdhRead.as_int(event.get("button_index", MOUSE_BUTTON_LEFT), MOUSE_BUTTON_LEFT)
286
340
  as MouseButton
287
341
  )
288
342
  built.append(mouse)
289
343
 
290
344
  "InputEventJoypadButton":
291
345
  var pad: InputEventJoypadButton = InputEventJoypadButton.new()
292
- pad.button_index = Read.as_int(event.get("button_index", 0)) as JoyButton
346
+ pad.button_index = GdhRead.as_int(event.get("button_index", 0)) as JoyButton
293
347
  built.append(pad)
294
348
 
295
349
  "InputEventJoypadMotion":
296
350
  var motion: InputEventJoypadMotion = InputEventJoypadMotion.new()
297
- motion.axis = Read.as_int(event.get("axis", 0)) as JoyAxis
298
- motion.axis_value = Read.as_float(event.get("axis_value", 1.0), 1.0)
351
+ motion.axis = GdhRead.as_int(event.get("axis", 0)) as JoyAxis
352
+ motion.axis_value = GdhRead.as_float(event.get("axis_value", 1.0), 1.0)
299
353
  built.append(motion)
300
354
 
301
355
  _:
@@ -305,6 +359,24 @@ func build_input_action(deadzone: float, events: Array) -> Dictionary:
305
359
 
306
360
 
307
361
  # The keycode for a key name as the tool spells it, or KEY_NONE for a name the table lacks.
362
+ ## The refusal for [param key_name], which names no key: the names nearest it, then every name taken,
363
+ ## since a name the caller has to guess at is the same guess again.
364
+ static func _unknown_key(key_name: String) -> String:
365
+ var every: PackedStringArray = []
366
+ var named: PackedStringArray = []
367
+ for name: String in KEY_NAMES:
368
+ var _every: bool = every.append(name)
369
+ if name.length() > 1 and not (name.begins_with("F") and name.substr(1).is_valid_int()):
370
+ var _named: bool = named.append(name)
371
+ var nearest: PackedStringArray = GdhRead.nearest(key_name, every)
372
+ var said: String = "Unknown key: %s." % key_name
373
+ if nearest.size() == 1:
374
+ said += " The nearest key name is %s." % nearest[0]
375
+ elif not nearest.is_empty():
376
+ said += " The nearest key names are %s." % ", ".join(nearest)
377
+ return said + " A key is named by its letter, its digit, F1 to F12, or one of: %s." % ", ".join(named)
378
+
379
+
308
380
  func _keycode_value(key_name: String) -> int:
309
381
  var wanted: String = key_name.to_lower()
310
382
  for name: String in KEY_NAMES:
@@ -3,7 +3,7 @@ extends RefCounted
3
3
  # Everything printed is diagnostics; the answer goes to a file. The server reads `[ERROR]` lines as
4
4
  # the operation's own errors, and the prefixes let a human read the rest without parsing it.
5
5
  #
6
- # Every module preloads this as `Log`. `Logger` is a native class from Godot 4.5 on, and a
6
+ # Every module preloads this as `GdhLog`. `Logger` is a native class from Godot 4.5 on, and a
7
7
  # constant of that name is a parse error rather than a shadowing warning.
8
8
 
9
9
  var debug_mode: bool = false
@@ -1,13 +1,13 @@
1
1
  extends RefCounted
2
2
 
3
- const Patterns = preload("patterns.gd")
4
- const Log = preload("logger.gd")
5
- const ProjectFile = preload("project_file.gd")
3
+ const GdhPatterns = preload("patterns.gd")
4
+ const GdhLog = preload("logger.gd")
5
+ const GdhProjectFile = preload("project_file.gd")
6
6
 
7
- var _log: Log
7
+ var _log: GdhLog
8
8
 
9
9
 
10
- func _init(p_log: Log) -> void:
10
+ func _init(p_log: GdhLog) -> void:
11
11
  _log = p_log
12
12
 
13
13
 
@@ -148,7 +148,7 @@ func _enabled_paths() -> Array[String]:
148
148
  paths.assign(packed)
149
149
  elif enabled_value is String:
150
150
  var listed: String = enabled_value
151
- for m: RegExMatch in Patterns.compiled("res://[^\"',)]+?plugin\\.cfg").search_all(listed):
151
+ for m: RegExMatch in GdhPatterns.compiled("res://[^\"',)]+?plugin\\.cfg").search_all(listed):
152
152
  paths.append(m.get_string())
153
153
  return paths
154
154
 
@@ -172,11 +172,11 @@ static func _names_of(paths: Array[String]) -> Array[String]:
172
172
  func _save_enabled_paths(paths: Array[String]) -> Error:
173
173
  if paths.is_empty():
174
174
  ProjectSettings.set_setting("editor_plugins/enabled", null)
175
- return ProjectFile.save()
175
+ return GdhProjectFile.save()
176
176
 
177
177
  # Kept in an Array[String] and converted here, because appending to a packed array answers with
178
178
  # whether it worked and a project holding return_value_discarded at error level will not compile
179
179
  # a script that drops that answer. The conversion is what matters: an Array[String] handed to
180
180
  # set_setting is written as Array[String]([...]), which the editor does not read as a plugin list.
181
181
  ProjectSettings.set_setting("editor_plugins/enabled", PackedStringArray(paths))
182
- return ProjectFile.save()
182
+ return GdhProjectFile.save()