gdharness 1.3.9 → 1.3.11
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 +4740 -3212
- package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
- package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +102 -18
- package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
- package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
- package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
- package/build/godot/addons/gdharness_editor/reading.gd +51 -1
- package/build/godot/addons/gdharness_editor/scene_file.gd +77 -140
- package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
- package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +11 -22
- package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
- package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +28 -5
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +39 -20
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -362
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +147 -492
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -3
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +79 -5
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +183 -37
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
- package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +51 -1
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +185 -30
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +86 -79
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +309 -22
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +163 -20
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +5 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +146 -5
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +11 -22
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
- package/build/godot/operations/animation_reads.gd +115 -4
- package/build/godot/operations/audio_buses.gd +93 -7
- package/build/godot/operations/classdb_queries.gd +119 -7
- package/build/godot/operations/declared_values.gd +272 -0
- package/build/godot/operations/dependencies.gd +154 -41
- package/build/godot/operations/editor_tools.gd +22 -14
- package/build/godot/operations/export_presets.gd +602 -4
- package/build/godot/operations/file_reads.gd +102 -18
- package/build/godot/operations/gdscript_authoring.gd +11 -1
- package/build/godot/operations/godot_operations.gd +16 -2
- package/build/godot/operations/import_pipeline.gd +20 -221
- package/build/godot/operations/input_actions.gd +108 -47
- package/build/godot/operations/project_config.gd +83 -69
- package/build/godot/operations/project_diagnostics.gd +4 -0
- package/build/godot/operations/project_validation.gd +266 -0
- package/build/godot/operations/property_paths.gd +89 -16
- package/build/godot/operations/property_values.gd +180 -121
- package/build/godot/operations/reading.gd +51 -1
- package/build/godot/operations/scene_connections.gd +222 -0
- package/build/godot/operations/serialisation.gd +11 -22
- package/build/godot/operations/setting_reads.gd +121 -0
- package/build/godot/operations/value_shapes.gd +128 -1
- package/build/godot/tests/listed.gd +73 -13
- package/build/keeper.js +1371 -706
- package/package.json +1 -1
|
@@ -5,6 +5,13 @@ const GdhLog = preload("logger.gd")
|
|
|
5
5
|
const GdhProjectFile = preload("project_file.gd")
|
|
6
6
|
## The device an event matches every device with, InputMap's ALL_DEVICES, which GDScript is not given.
|
|
7
7
|
const ALL_DEVICES: int = -1
|
|
8
|
+
const MOUSE_NAMES: String = (
|
|
9
|
+
"1 left, 2 right, 3 middle, 4 and 5 the wheel up and down, 6 and 7 the wheel left and right,"
|
|
10
|
+
+ " 8 and 9 the side buttons"
|
|
11
|
+
)
|
|
12
|
+
const AXIS_NAMES: String = (
|
|
13
|
+
"0 and 1 the left stick across and down, 2 and 3 the right stick," + " 4 and 5 the triggers"
|
|
14
|
+
)
|
|
8
15
|
|
|
9
16
|
# The key names a caller may write, and what the engine calls them.
|
|
10
17
|
const KEY_NAMES: Dictionary = {
|
|
@@ -147,49 +154,11 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
147
154
|
if not event is Dictionary:
|
|
148
155
|
return _log.failure("Every event must be an object, not " + JSON.stringify(event))
|
|
149
156
|
var fields: Dictionary = event
|
|
150
|
-
var
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if keycode.is_empty():
|
|
156
|
-
return _log.failure("A key event needs keycode")
|
|
157
|
-
var key_value: int = _keycode_value(keycode)
|
|
158
|
-
if key_value == KEY_NONE:
|
|
159
|
-
return _log.failure(_unknown_key(keycode))
|
|
160
|
-
|
|
161
|
-
var event_config: Dictionary = {"class_name": "InputEventKey"}
|
|
162
|
-
var physical: bool = GdhRead.as_bool(fields.get("physical", false))
|
|
163
|
-
event_config["physical_keycode" if physical else "keycode"] = key_value
|
|
164
|
-
if fields.get("ctrl", false):
|
|
165
|
-
event_config["ctrl_pressed"] = true
|
|
166
|
-
if fields.get("alt", false):
|
|
167
|
-
event_config["alt_pressed"] = true
|
|
168
|
-
if fields.get("shift", false):
|
|
169
|
-
event_config["shift_pressed"] = true
|
|
170
|
-
events_config.append(event_config)
|
|
171
|
-
|
|
172
|
-
"mouse_button":
|
|
173
|
-
events_config.append(
|
|
174
|
-
{"class_name": "InputEventMouseButton", "button_index": fields.get("button", 1)}
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
"joypad_button":
|
|
178
|
-
events_config.append(
|
|
179
|
-
{"class_name": "InputEventJoypadButton", "button_index": fields.get("button", 0)}
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
"joypad_axis":
|
|
183
|
-
var motion: Dictionary = {
|
|
184
|
-
"class_name": "InputEventJoypadMotion",
|
|
185
|
-
"axis": fields.get("axis", 0),
|
|
186
|
-
"axis_value": fields.get("axisValue", 1),
|
|
187
|
-
}
|
|
188
|
-
events_config.append(motion)
|
|
189
|
-
|
|
190
|
-
_:
|
|
191
|
-
var known: String = "key, mouse_button, joypad_button, joypad_axis"
|
|
192
|
-
return _log.failure("Unknown event type: " + event_type + ". One of " + known + ".")
|
|
157
|
+
var built: Variant = _event_config(fields)
|
|
158
|
+
if built is String:
|
|
159
|
+
return _log.failure(str(built))
|
|
160
|
+
var event_config: Dictionary = built
|
|
161
|
+
events_config.append(event_config)
|
|
193
162
|
|
|
194
163
|
if events_config.is_empty():
|
|
195
164
|
return _log.failure("events must hold at least one event")
|
|
@@ -303,14 +272,28 @@ func remove_input_action(params: Dictionary) -> Dictionary:
|
|
|
303
272
|
return _log.failure("This project has no input action named " + action_name)
|
|
304
273
|
# The engine registers its own ui_ actions with a default, so clearing one from project.godot
|
|
305
274
|
# puts the engine's binding back rather than removing the action.
|
|
306
|
-
|
|
275
|
+
var engine_binding: Variant = ProjectSettings.property_get_revert(setting)
|
|
276
|
+
if engine_binding != null:
|
|
277
|
+
# At the engine's binding the file holds no line for it, and the refusal for a rebound one
|
|
278
|
+
# spoke of a binding the project had never changed.
|
|
279
|
+
if ProjectSettings.get_setting(setting) == engine_binding:
|
|
280
|
+
return _log.failure(
|
|
281
|
+
(
|
|
282
|
+
(
|
|
283
|
+
"%s is one of the engine's own actions, and project.godot does not rebind it, so "
|
|
284
|
+
+ "there is nothing to remove: the engine's binding holds. Rebind it with "
|
|
285
|
+
+ "add_input_action."
|
|
286
|
+
)
|
|
287
|
+
% action_name
|
|
288
|
+
)
|
|
289
|
+
)
|
|
307
290
|
return _log.failure(
|
|
308
291
|
(
|
|
309
292
|
(
|
|
310
|
-
"%s is one of the engine's own actions
|
|
311
|
-
+ "engine's binding
|
|
293
|
+
"%s is one of the engine's own actions, so it cannot be removed. project_settings "
|
|
294
|
+
+ "unset %s puts the engine's binding back, and add_input_action rebinds it."
|
|
312
295
|
)
|
|
313
|
-
% action_name
|
|
296
|
+
% [action_name, setting]
|
|
314
297
|
)
|
|
315
298
|
)
|
|
316
299
|
ProjectSettings.set_setting(setting, null)
|
|
@@ -376,6 +359,84 @@ func build_input_action(deadzone: float, events: Array) -> Dictionary:
|
|
|
376
359
|
# The keycode for a key name as the tool spells it, or KEY_NONE for a name the table lacks.
|
|
377
360
|
## The refusal for [param key_name], which names no key: the names nearest it, then every name taken,
|
|
378
361
|
## since a name the caller has to guess at is the same guess again.
|
|
362
|
+
## The event [param fields] describe, as [method build_input_action] takes it, or why it is no event.
|
|
363
|
+
func _event_config(fields: Dictionary) -> Variant:
|
|
364
|
+
var event_type: String = str(fields.get("type", ""))
|
|
365
|
+
match event_type:
|
|
366
|
+
"key":
|
|
367
|
+
var keycode: String = str(fields.get("keycode", ""))
|
|
368
|
+
if keycode.is_empty():
|
|
369
|
+
return "A key event needs keycode"
|
|
370
|
+
var key_value: int = _keycode_value(keycode)
|
|
371
|
+
if key_value == KEY_NONE:
|
|
372
|
+
return _unknown_key(keycode)
|
|
373
|
+
var event_config: Dictionary = {"class_name": "InputEventKey"}
|
|
374
|
+
var physical: bool = GdhRead.as_bool(fields.get("physical", false))
|
|
375
|
+
event_config["physical_keycode" if physical else "keycode"] = key_value
|
|
376
|
+
for modifier: String in ["ctrl", "alt", "shift"]:
|
|
377
|
+
if GdhRead.as_bool(fields.get(modifier, false)):
|
|
378
|
+
event_config[modifier + "_pressed"] = true
|
|
379
|
+
return event_config
|
|
380
|
+
"mouse_button":
|
|
381
|
+
var mouse: Variant = _whole_in(
|
|
382
|
+
fields, "button", MOUSE_BUTTON_LEFT, MOUSE_BUTTON_XBUTTON2, MOUSE_NAMES
|
|
383
|
+
)
|
|
384
|
+
if mouse is String:
|
|
385
|
+
return "A mouse_button event " + str(mouse)
|
|
386
|
+
return {"class_name": "InputEventMouseButton", "button_index": mouse}
|
|
387
|
+
"joypad_button":
|
|
388
|
+
var pad: Variant = _whole_in(fields, "button", 0, JOY_BUTTON_MAX - 1, "0 is A, 1 B, 2 X, 3 Y")
|
|
389
|
+
if pad is String:
|
|
390
|
+
return "A joypad_button event " + str(pad)
|
|
391
|
+
return {"class_name": "InputEventJoypadButton", "button_index": pad}
|
|
392
|
+
"joypad_axis":
|
|
393
|
+
var axis: Variant = _whole_in(fields, "axis", 0, JOY_AXIS_MAX - 1, AXIS_NAMES)
|
|
394
|
+
if axis is String:
|
|
395
|
+
return "A joypad_axis event " + str(axis)
|
|
396
|
+
# The direction is half the binding: left on a stick and right on it are two actions, and the
|
|
397
|
+
# editor binds only a whole push one way, so a guessed direction or part of one is a binding
|
|
398
|
+
# nobody asked for.
|
|
399
|
+
var direction: Variant = fields.get("axisValue")
|
|
400
|
+
var numeric: bool = direction is int or direction is float
|
|
401
|
+
if not numeric or absf(GdhRead.as_float(direction)) != 1.0:
|
|
402
|
+
return (
|
|
403
|
+
"A joypad_axis event needs axisValue, -1 or 1, the way the axis is pushed: %s was given."
|
|
404
|
+
% _shown(direction)
|
|
405
|
+
)
|
|
406
|
+
return {
|
|
407
|
+
"class_name": "InputEventJoypadMotion",
|
|
408
|
+
"axis": axis,
|
|
409
|
+
"axis_value": GdhRead.as_float(direction)
|
|
410
|
+
}
|
|
411
|
+
var known: String = "key, mouse_button, joypad_button, joypad_axis"
|
|
412
|
+
return "Unknown event type: " + event_type + ". One of " + known + "."
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
## The whole number [param fields] holds under [param key], from [param least] to [param most], or
|
|
416
|
+
## the rest of a refusal saying what it takes and what [param named] each number is. A field left out
|
|
417
|
+
## was given a default, a left click for a mouse event and axis 0 for a stick, and written as bound.
|
|
418
|
+
static func _whole_in(fields: Dictionary, key: String, least: int, most: int, named: String) -> Variant:
|
|
419
|
+
var held: Variant = fields.get(key)
|
|
420
|
+
if held is int or held is float:
|
|
421
|
+
var number: float = GdhRead.as_float(held)
|
|
422
|
+
if number == floorf(number) and number >= least and number <= most:
|
|
423
|
+
return int(number)
|
|
424
|
+
return (
|
|
425
|
+
"needs %s, a whole number from %d to %d (%s): %s was given." % [key, least, most, named, _shown(held)]
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
## [param held] as the caller wrote it: JSON has one kind of number, so 10 arrives as 10.0.
|
|
430
|
+
static func _shown(held: Variant) -> String:
|
|
431
|
+
if held == null:
|
|
432
|
+
return "none"
|
|
433
|
+
if held is float:
|
|
434
|
+
var number: float = held
|
|
435
|
+
if is_finite(number) and number == floorf(number):
|
|
436
|
+
return str(int(number))
|
|
437
|
+
return JSON.stringify(held)
|
|
438
|
+
|
|
439
|
+
|
|
379
440
|
static func _unknown_key(key_name: String) -> String:
|
|
380
441
|
var every: PackedStringArray = []
|
|
381
442
|
var named: PackedStringArray = []
|
|
@@ -5,6 +5,7 @@ const GdhRead = preload("reading.gd")
|
|
|
5
5
|
const GdhLog = preload("logger.gd")
|
|
6
6
|
const GdhProjectFile = preload("project_file.gd")
|
|
7
7
|
const GdhSerialisation = preload("serialisation.gd")
|
|
8
|
+
const GdhSettingReads = preload("setting_reads.gd")
|
|
8
9
|
const GdhShapes = preload("value_shapes.gd")
|
|
9
10
|
|
|
10
11
|
var _log: GdhLog
|
|
@@ -19,7 +20,8 @@ func _init(p_log: GdhLog) -> void:
|
|
|
19
20
|
func get_project_setting(params: Dictionary) -> Dictionary:
|
|
20
21
|
var prefix: String = str(params.get("prefix", ""))
|
|
21
22
|
if not prefix.is_empty():
|
|
22
|
-
|
|
23
|
+
_log.info("Listing project settings under: " + prefix)
|
|
24
|
+
return GdhSettingReads.under(prefix, _values)
|
|
23
25
|
|
|
24
26
|
var setting_path: String = str(params.get("setting", ""))
|
|
25
27
|
if setting_path.is_empty():
|
|
@@ -27,73 +29,7 @@ func get_project_setting(params: Dictionary) -> Dictionary:
|
|
|
27
29
|
|
|
28
30
|
_log.info("Getting project setting: " + setting_path)
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
"setting_path": setting_path, "exists": ProjectSettings.has_setting(setting_path)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if result["exists"]:
|
|
35
|
-
var value: Variant = ProjectSettings.get_setting(setting_path)
|
|
36
|
-
result["value"] = _values.serialize_value(value)
|
|
37
|
-
# A setting the engine registers exists whether or not the project sets it, so exists did
|
|
38
|
-
# not say whether project.godot has a line for it. At its default it has none: the engine
|
|
39
|
-
# drops such a key when it saves.
|
|
40
|
-
var default_value: Variant = ProjectSettings.property_get_revert(setting_path)
|
|
41
|
-
if default_value != null:
|
|
42
|
-
result["at_default"] = value == default_value
|
|
43
|
-
else:
|
|
44
|
-
result["value"] = null
|
|
45
|
-
result["message"] = "Setting does not exist"
|
|
46
|
-
|
|
47
|
-
# The value under this name is not the one every build reads: `name.windows` replaces it on
|
|
48
|
-
# Windows, `name.editor` in the editor. Read alone, a caller saw the plain value and not the one
|
|
49
|
-
# their game showed.
|
|
50
|
-
var overrides: Dictionary = {}
|
|
51
|
-
for info: Dictionary in ProjectSettings.get_property_list():
|
|
52
|
-
var name: String = str(info.get("name", ""))
|
|
53
|
-
if name.begins_with(setting_path + "."):
|
|
54
|
-
overrides[name.substr(setting_path.length() + 1)] = _values.serialize_value(
|
|
55
|
-
ProjectSettings.get_setting(name)
|
|
56
|
-
)
|
|
57
|
-
if not overrides.is_empty():
|
|
58
|
-
result["overrides"] = overrides
|
|
59
|
-
|
|
60
|
-
return result
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# Every setting whose name starts with [prefix], with the type the engine registers for each.
|
|
64
|
-
#
|
|
65
|
-
# A caller after a whole family, "everything under debug/gdscript/warnings", had no way to ask: a get
|
|
66
|
-
# takes one name, and the engine is the only thing that knows what the family contains. The way round
|
|
67
|
-
# it was to walk ProjectSettings.get_property_list() from a script written for the purpose, which is
|
|
68
|
-
# an engine start to answer a question the engine was already open for.
|
|
69
|
-
#
|
|
70
|
-
# The type is here because it is the half a name hides. A setting sitting among a family of levels
|
|
71
|
-
# can be a bool, and a caller that assumes otherwise writes a level over it and sees nothing go
|
|
72
|
-
# wrong: 2 is as true as true is. Reading it out of the property list is how that is caught.
|
|
73
|
-
func _settings_under(prefix: String) -> Dictionary:
|
|
74
|
-
_log.info("Listing project settings under: " + prefix)
|
|
75
|
-
|
|
76
|
-
var found: Array[Dictionary] = []
|
|
77
|
-
for property: Dictionary in ProjectSettings.get_property_list():
|
|
78
|
-
var name: String = str(property.get("name", ""))
|
|
79
|
-
if not name.begins_with(prefix):
|
|
80
|
-
continue
|
|
81
|
-
var kind: int = GdhRead.as_int(property.get("type", TYPE_NIL), TYPE_NIL)
|
|
82
|
-
var value: Variant = ProjectSettings.get_setting(name)
|
|
83
|
-
var described: Dictionary = {
|
|
84
|
-
"setting": name,
|
|
85
|
-
"type": type_string(kind),
|
|
86
|
-
"value": _values.serialize_value(value),
|
|
87
|
-
}
|
|
88
|
-
# As a single get says it, so a family read can be told apart into what the project sets
|
|
89
|
-
# and the engine's defaults: "input/" is ninety actions the engine ships and the project's few.
|
|
90
|
-
var default_value: Variant = ProjectSettings.property_get_revert(name)
|
|
91
|
-
if default_value != null:
|
|
92
|
-
described["at_default"] = value == default_value
|
|
93
|
-
found.append(described)
|
|
94
|
-
|
|
95
|
-
found.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["setting"] < b["setting"])
|
|
96
|
-
return {"prefix": prefix, "count": found.size(), "settings": found}
|
|
32
|
+
return GdhSettingReads.one(setting_path, _values)
|
|
97
33
|
|
|
98
34
|
|
|
99
35
|
# Set a project setting value
|
|
@@ -146,6 +82,14 @@ func set_project_setting(params: Dictionary) -> Dictionary:
|
|
|
146
82
|
if had_value and typeof(old_value) == typeof(final_value) and old_value == final_value:
|
|
147
83
|
answer["saved"] = false
|
|
148
84
|
answer["unchanged"] = true
|
|
85
|
+
if was_default:
|
|
86
|
+
answer["note"] = (
|
|
87
|
+
(
|
|
88
|
+
"%s is already this setting's default, which project.godot holds no line for:"
|
|
89
|
+
+ " the engine reads it wherever %s is asked for, so nothing was written."
|
|
90
|
+
)
|
|
91
|
+
% [JSON.stringify(_values.serialize_value(final_value)), setting_path]
|
|
92
|
+
)
|
|
149
93
|
return answer
|
|
150
94
|
|
|
151
95
|
var near: PackedStringArray = []
|
|
@@ -159,6 +103,24 @@ func set_project_setting(params: Dictionary) -> Dictionary:
|
|
|
159
103
|
)
|
|
160
104
|
% [setting_path, "setting is" if near.size() == 1 else "settings are", ", ".join(near)]
|
|
161
105
|
)
|
|
106
|
+
# Written as given, since a file can be made after the setting names it, as an import makes a
|
|
107
|
+
# translation; said, since until then the engine complains of it on every boot.
|
|
108
|
+
var missing: Array[String] = _missing_files(final_value)
|
|
109
|
+
if not missing.is_empty():
|
|
110
|
+
answer["missing_files"] = missing
|
|
111
|
+
var said: String = (
|
|
112
|
+
"%s %s not exist, and the engine reports %s on every boot until %s"
|
|
113
|
+
% [
|
|
114
|
+
", ".join(missing),
|
|
115
|
+
"does" if missing.size() == 1 else "do",
|
|
116
|
+
"it" if missing.size() == 1 else "them",
|
|
117
|
+
"it does" if missing.size() == 1 else "they do",
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
var made: Array[String] = _translations_made_by(missing)
|
|
121
|
+
if not made.is_empty():
|
|
122
|
+
said += ("; project_import reimport makes the translation files from " + ", ".join(made))
|
|
123
|
+
answer["note"] = (str(answer["note"]) + " " if answer.has("note") else "") + said + "."
|
|
162
124
|
ProjectSettings.set_setting(setting_path, final_value)
|
|
163
125
|
var err: Error = GdhProjectFile.save()
|
|
164
126
|
if err != OK:
|
|
@@ -166,6 +128,45 @@ func set_project_setting(params: Dictionary) -> Dictionary:
|
|
|
166
128
|
return answer
|
|
167
129
|
|
|
168
130
|
|
|
131
|
+
## The res:// files [param value] names that are not on disk: a path, an autoload's starred path, or
|
|
132
|
+
## a list or dictionary of them.
|
|
133
|
+
static func _missing_files(value: Variant) -> Array[String]:
|
|
134
|
+
var items: Array = []
|
|
135
|
+
if value is String or value is StringName:
|
|
136
|
+
items = [value]
|
|
137
|
+
elif value is PackedStringArray:
|
|
138
|
+
var strings: PackedStringArray = value
|
|
139
|
+
items = Array(strings)
|
|
140
|
+
elif value is Array:
|
|
141
|
+
items = value
|
|
142
|
+
elif value is Dictionary:
|
|
143
|
+
var held: Dictionary = value
|
|
144
|
+
items = held.values()
|
|
145
|
+
var missing: Array[String] = []
|
|
146
|
+
for item: Variant in items:
|
|
147
|
+
if not (item is String or item is StringName):
|
|
148
|
+
continue
|
|
149
|
+
var path: String = str(item).trim_prefix("*")
|
|
150
|
+
if not path.begins_with("res://"):
|
|
151
|
+
continue
|
|
152
|
+
if not FileAccess.file_exists(path) and not DirAccess.dir_exists_absolute(path):
|
|
153
|
+
missing.append(path)
|
|
154
|
+
return missing
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
## The CSV files an import makes the translations among [param missing] from: strings.en.translation
|
|
158
|
+
## from strings.csv beside it.
|
|
159
|
+
static func _translations_made_by(missing: Array[String]) -> Array[String]:
|
|
160
|
+
var sources: Array[String] = []
|
|
161
|
+
for path: String in missing:
|
|
162
|
+
if path.get_extension() != "translation":
|
|
163
|
+
continue
|
|
164
|
+
var csv: String = path.get_basename().get_basename() + ".csv"
|
|
165
|
+
if FileAccess.file_exists(csv) and not csv in sources:
|
|
166
|
+
sources.append(csv)
|
|
167
|
+
return sources
|
|
168
|
+
|
|
169
|
+
|
|
169
170
|
# Takes a setting's line out of project.godot, so the engine's default holds again, or nothing at all
|
|
170
171
|
# for one the engine does not declare: the way back from a set, which writes a misspelt name as given.
|
|
171
172
|
func unset_project_setting(params: Dictionary) -> Dictionary:
|
|
@@ -184,7 +185,7 @@ func unset_project_setting(params: Dictionary) -> Dictionary:
|
|
|
184
185
|
answer["removed"] = false
|
|
185
186
|
answer["saved"] = false
|
|
186
187
|
answer["note"] = "project.godot does not set %s, so there was nothing to take out." % setting_path
|
|
187
|
-
return answer
|
|
188
|
+
return _with_what_holds(answer, setting_path)
|
|
188
189
|
answer["old_value"] = _values.serialize_value(value)
|
|
189
190
|
# The default rather than null for a setting the engine declares: null takes the declaration away
|
|
190
191
|
# as well, and a value at its default is left out of the file all the same.
|
|
@@ -194,6 +195,19 @@ func unset_project_setting(params: Dictionary) -> Dictionary:
|
|
|
194
195
|
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
195
196
|
answer["removed"] = true
|
|
196
197
|
answer["saved"] = true
|
|
198
|
+
return _with_what_holds(answer, setting_path)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# An override has no default of its own: with it gone, a build with its feature reads the base
|
|
202
|
+
# setting, and default_value, null, said nothing of that.
|
|
203
|
+
func _with_what_holds(answer: Dictionary, setting_path: String) -> Dictionary:
|
|
204
|
+
var base: String = GdhSettingReads.override_base(setting_path)
|
|
205
|
+
if base.is_empty() or not ProjectSettings.has_setting(base):
|
|
206
|
+
return answer
|
|
207
|
+
answer["override_of"] = base
|
|
208
|
+
answer["holds"] = _values.serialize_value(ProjectSettings.get_setting(base))
|
|
209
|
+
var said: String = GdhSettingReads.overridden_by_none(setting_path, _values)
|
|
210
|
+
answer["note"] = said if not answer.has("note") else "%s %s" % [answer["note"], said]
|
|
197
211
|
return answer
|
|
198
212
|
|
|
199
213
|
|
|
@@ -5,6 +5,7 @@ const GdhFileWalk = preload("file_walk.gd")
|
|
|
5
5
|
const GdhImportPipeline = preload("import_pipeline.gd")
|
|
6
6
|
const GdhLog = preload("logger.gd")
|
|
7
7
|
const GdhRead = preload("reading.gd")
|
|
8
|
+
const GdhSceneConnections = preload("scene_connections.gd")
|
|
8
9
|
const GdhScriptCheck = preload("script_check.gd")
|
|
9
10
|
|
|
10
11
|
var _log: GdhLog
|
|
@@ -129,6 +130,9 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
129
130
|
if scene_files.size() == 0:
|
|
130
131
|
details.append("No scene files found in project")
|
|
131
132
|
deductions += 5
|
|
133
|
+
for entry: Dictionary in GdhSceneConnections.new(_log).broken():
|
|
134
|
+
details.append(GdhSceneConnections.broken_line(entry))
|
|
135
|
+
deductions += 5
|
|
132
136
|
|
|
133
137
|
checks["scenes"] = {
|
|
134
138
|
"name": "Scenes",
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Whether a project is ready to run and export: its settings, main scene, imports, presets,
|
|
4
|
+
## scripts, references and signal connections, each named under checks_performed.
|
|
5
|
+
|
|
6
|
+
const GdhDependencies = preload("dependencies.gd")
|
|
7
|
+
const GdhExportPresets = preload("export_presets.gd")
|
|
8
|
+
const GdhFileWalk = preload("file_walk.gd")
|
|
9
|
+
const GdhImportPipeline = preload("import_pipeline.gd")
|
|
10
|
+
const GdhLog = preload("logger.gd")
|
|
11
|
+
const GdhRead = preload("reading.gd")
|
|
12
|
+
const GdhSceneConnections = preload("scene_connections.gd")
|
|
13
|
+
const GdhScriptCheck = preload("script_check.gd")
|
|
14
|
+
const CONNECTION_FIX: String = (
|
|
15
|
+
"Write the method or signal, or point the connection at the one that replaced it with" + " scene_signal"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
var _log: GdhLog
|
|
19
|
+
var _files: GdhFileWalk = GdhFileWalk.new()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
func _init(p_log: GdhLog) -> void:
|
|
23
|
+
_log = p_log
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
func validate_project(params: Dictionary) -> Dictionary:
|
|
27
|
+
var preset_name: String = str(params.get("preset", ""))
|
|
28
|
+
var include_suggestions: bool = GdhRead.as_bool(params.get("include_suggestions", true), true)
|
|
29
|
+
|
|
30
|
+
_log.info("Validating project" + (" for preset: " + preset_name if not preset_name.is_empty() else ""))
|
|
31
|
+
|
|
32
|
+
var issues: Array[Dictionary] = []
|
|
33
|
+
var warnings: Array[Dictionary] = []
|
|
34
|
+
var checks_performed: Array[String] = []
|
|
35
|
+
|
|
36
|
+
checks_performed.append("project_file")
|
|
37
|
+
if not FileAccess.file_exists("res://project.godot"):
|
|
38
|
+
issues.append(
|
|
39
|
+
_finding(
|
|
40
|
+
"error",
|
|
41
|
+
"project_file",
|
|
42
|
+
"project.godot not found",
|
|
43
|
+
"Ensure you are running this from a valid Godot project directory",
|
|
44
|
+
include_suggestions
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
checks_performed.append("main_scene")
|
|
49
|
+
var main_scene: String = str(ProjectSettings.get_setting("application/run/main_scene", ""))
|
|
50
|
+
if main_scene.is_empty():
|
|
51
|
+
issues.append(
|
|
52
|
+
_finding(
|
|
53
|
+
"error",
|
|
54
|
+
"main_scene",
|
|
55
|
+
"No main scene set",
|
|
56
|
+
"Set a main scene in Project Settings > Application > Run > Main Scene",
|
|
57
|
+
include_suggestions
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
elif not GdhFileWalk.missing_reason(main_scene).is_empty():
|
|
61
|
+
issues.append(
|
|
62
|
+
_finding(
|
|
63
|
+
"error",
|
|
64
|
+
"main_scene",
|
|
65
|
+
"Main scene " + GdhFileWalk.missing_reason(main_scene),
|
|
66
|
+
"Update the main scene setting or create the missing scene file",
|
|
67
|
+
include_suggestions
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# A file never imported is on disk, so the reference checks pass it, and still nothing loads it:
|
|
72
|
+
# a scene using one fails to load in a run or an export.
|
|
73
|
+
checks_performed.append("imports")
|
|
74
|
+
var never: Array[String] = []
|
|
75
|
+
for path: String in _files.find_files_with_extensions("res://", GdhImportPipeline.IMPORTABLE_EXTENSIONS):
|
|
76
|
+
if not FileAccess.file_exists(path + ".import"):
|
|
77
|
+
never.append(path)
|
|
78
|
+
if not never.is_empty():
|
|
79
|
+
never.sort()
|
|
80
|
+
warnings.append(
|
|
81
|
+
_finding(
|
|
82
|
+
"warning",
|
|
83
|
+
"imports",
|
|
84
|
+
GdhImportPipeline.never_imported_line(never),
|
|
85
|
+
"Run project_import reimport, or open the project in the editor, whose scan imports them",
|
|
86
|
+
include_suggestions
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
checks_performed.append("export_presets")
|
|
91
|
+
if not FileAccess.file_exists("res://export_presets.cfg"):
|
|
92
|
+
warnings.append(
|
|
93
|
+
_finding(
|
|
94
|
+
"warning",
|
|
95
|
+
"export_presets",
|
|
96
|
+
"No export presets configured",
|
|
97
|
+
"Add a preset with project_export add_preset, or in the editor: Project > Export",
|
|
98
|
+
include_suggestions
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# The preset asked about is looked for rather than only logged: validating "for" a preset that
|
|
103
|
+
# does not exist answered valid, as though it had been checked.
|
|
104
|
+
if not preset_name.is_empty():
|
|
105
|
+
checks_performed.append("export_preset")
|
|
106
|
+
var named: Array[String] = GdhExportPresets.names()
|
|
107
|
+
if preset_name not in named:
|
|
108
|
+
issues.append(
|
|
109
|
+
_finding(
|
|
110
|
+
"error",
|
|
111
|
+
"export_preset",
|
|
112
|
+
(
|
|
113
|
+
"No export preset is named "
|
|
114
|
+
+ preset_name
|
|
115
|
+
+ (
|
|
116
|
+
": the project has none"
|
|
117
|
+
if named.is_empty()
|
|
118
|
+
else ": the presets are " + ", ".join(named)
|
|
119
|
+
)
|
|
120
|
+
),
|
|
121
|
+
"Pass one of the preset names, or add the preset with project_export add_preset",
|
|
122
|
+
include_suggestions
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
var gone: Array[String] = GdhExportPresets.missing_named(preset_name)
|
|
127
|
+
if not gone.is_empty():
|
|
128
|
+
warnings.append(
|
|
129
|
+
_finding(
|
|
130
|
+
"warning",
|
|
131
|
+
"export_preset_files",
|
|
132
|
+
"The preset %s names files that are not there: %s" % [preset_name, "; ".join(gone)],
|
|
133
|
+
"Add the files, or point the options at others with project_export set_preset",
|
|
134
|
+
include_suggestions
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
checks_performed.append("icon")
|
|
139
|
+
var icon_path: String = str(ProjectSettings.get_setting("application/config/icon", ""))
|
|
140
|
+
if icon_path.is_empty():
|
|
141
|
+
warnings.append(
|
|
142
|
+
_finding(
|
|
143
|
+
"warning",
|
|
144
|
+
"icon",
|
|
145
|
+
"No application icon set",
|
|
146
|
+
"Set an icon in Project Settings > Application > Config > Icon",
|
|
147
|
+
include_suggestions
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
elif not FileAccess.file_exists(icon_path):
|
|
151
|
+
warnings.append(
|
|
152
|
+
_finding(
|
|
153
|
+
"warning",
|
|
154
|
+
"icon",
|
|
155
|
+
"Icon file does not exist: " + icon_path,
|
|
156
|
+
"Update the icon path or add the missing icon file",
|
|
157
|
+
include_suggestions
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
checks_performed.append("project_name")
|
|
162
|
+
var project_name: String = str(ProjectSettings.get_setting("application/config/name", ""))
|
|
163
|
+
if project_name.is_empty():
|
|
164
|
+
warnings.append(
|
|
165
|
+
_finding(
|
|
166
|
+
"warning",
|
|
167
|
+
"project_name",
|
|
168
|
+
"No project name set",
|
|
169
|
+
"Set a project name in Project Settings > Application > Config > Name",
|
|
170
|
+
include_suggestions
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
checks_performed.append("scripts")
|
|
175
|
+
var script_files: Array[String] = _files.find_files_with_extensions("res://", ["gd"])
|
|
176
|
+
var scripts_checked: int = 0
|
|
177
|
+
var script_issues: Array[Dictionary] = []
|
|
178
|
+
|
|
179
|
+
# A hundred scripts is enough to say whether the project is tidy without a large one
|
|
180
|
+
# turning validation into a full read of its source tree.
|
|
181
|
+
for script_path: String in script_files.slice(0, 100):
|
|
182
|
+
scripts_checked += 1
|
|
183
|
+
var file: FileAccess = FileAccess.open(script_path, FileAccess.READ)
|
|
184
|
+
if file:
|
|
185
|
+
var content: String = file.get_as_text()
|
|
186
|
+
file.close()
|
|
187
|
+
|
|
188
|
+
if "# TODO" in content or "# FIXME" in content:
|
|
189
|
+
script_issues.append({"path": script_path, "issue": "Contains TODO/FIXME comments"})
|
|
190
|
+
if "pass # TODO" in content:
|
|
191
|
+
script_issues.append({"path": script_path, "issue": "Contains unimplemented functions"})
|
|
192
|
+
|
|
193
|
+
# Compiled by the server and handed in, as for the health check: a script the engine refuses
|
|
194
|
+
# stops the export's game at the first scene using it, which the comment check above never saw.
|
|
195
|
+
var compiled: Dictionary = params.get("compiled", {})
|
|
196
|
+
if compiled.has("checked"):
|
|
197
|
+
checks_performed.append("compiles")
|
|
198
|
+
var failures: Array = compiled.get("failures", [])
|
|
199
|
+
for failure: Dictionary in failures:
|
|
200
|
+
issues.append(
|
|
201
|
+
_finding(
|
|
202
|
+
"error",
|
|
203
|
+
"compiles",
|
|
204
|
+
GdhScriptCheck.failure_line(failure),
|
|
205
|
+
"script_diagnostics gives every error in it with its line",
|
|
206
|
+
include_suggestions
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
checks_performed.append("references")
|
|
211
|
+
for entry: Dictionary in GdhDependencies.new(_log).missing_references():
|
|
212
|
+
issues.append(
|
|
213
|
+
_finding(
|
|
214
|
+
"error",
|
|
215
|
+
"references",
|
|
216
|
+
GdhDependencies.missing_line(entry),
|
|
217
|
+
"Restore the file, or point the reference at the file that replaced it",
|
|
218
|
+
include_suggestions
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# A connection whose method or signal is not there loads without a word and fails only once the
|
|
223
|
+
# game runs, on every emission or not at all; the references check sees files, not members.
|
|
224
|
+
checks_performed.append("connections")
|
|
225
|
+
for entry: Dictionary in GdhSceneConnections.new(_log).broken():
|
|
226
|
+
warnings.append(
|
|
227
|
+
_finding(
|
|
228
|
+
"warning",
|
|
229
|
+
"connections",
|
|
230
|
+
GdhSceneConnections.broken_line(entry),
|
|
231
|
+
CONNECTION_FIX,
|
|
232
|
+
include_suggestions
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if script_issues.size() > 0:
|
|
237
|
+
var warning: Dictionary = _finding(
|
|
238
|
+
"warning",
|
|
239
|
+
"scripts",
|
|
240
|
+
str(script_issues.size()) + " script issues found",
|
|
241
|
+
"Review and resolve TODO/FIXME items before release",
|
|
242
|
+
include_suggestions
|
|
243
|
+
)
|
|
244
|
+
warning["details"] = script_issues.slice(0, 5)
|
|
245
|
+
warnings.append(warning)
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
"valid": issues.is_empty(),
|
|
249
|
+
"issues": issues,
|
|
250
|
+
"warnings": warnings,
|
|
251
|
+
"checks_performed": checks_performed,
|
|
252
|
+
"scripts_checked": scripts_checked,
|
|
253
|
+
# Said beside the count, so a project of four hundred scripts is not read as checked whole.
|
|
254
|
+
"scripts_total": script_files.size(),
|
|
255
|
+
"issue_count": issues.size(),
|
|
256
|
+
"warning_count": warnings.size()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
func _finding(
|
|
261
|
+
type: String, check: String, message: String, suggestion: String, include_suggestion: bool
|
|
262
|
+
) -> Dictionary:
|
|
263
|
+
var finding: Dictionary = {"type": type, "check": check, "message": message}
|
|
264
|
+
if include_suggestion:
|
|
265
|
+
finding["suggestion"] = suggestion
|
|
266
|
+
return finding
|