gdharness 1.3.8 → 1.3.10
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 +4668 -3229
- 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 +115 -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 +112 -6
- package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
- 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 +19 -29
- 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 +39 -6
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
- 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 +162 -483
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
- 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/call_errors.gd +8 -2
- package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
- 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 +320 -27
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
- 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 +19 -29
- 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 +29 -13
- package/build/godot/operations/export_presets.gd +602 -4
- package/build/godot/operations/file_reads.gd +115 -18
- package/build/godot/operations/gdscript_analysis.gd +14 -0
- package/build/godot/operations/gdscript_authoring.gd +12 -2
- package/build/godot/operations/godot_operations.gd +18 -2
- package/build/godot/operations/import_pipeline.gd +20 -221
- package/build/godot/operations/input_actions.gd +127 -51
- package/build/godot/operations/project_config.gd +113 -68
- 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 +112 -6
- package/build/godot/operations/scene_connections.gd +222 -0
- package/build/godot/operations/serialisation.gd +19 -29
- package/build/godot/operations/setting_reads.gd +121 -0
- package/build/godot/operations/value_shapes.gd +128 -1
- package/build/godot/tests/listed.gd +20 -13
- package/build/keeper.js +1402 -707
- package/package.json +1 -1
|
@@ -3,6 +3,15 @@ extends RefCounted
|
|
|
3
3
|
const GdhRead = preload("reading.gd")
|
|
4
4
|
const GdhLog = preload("logger.gd")
|
|
5
5
|
const GdhProjectFile = preload("project_file.gd")
|
|
6
|
+
## The device an event matches every device with, InputMap's ALL_DEVICES, which GDScript is not given.
|
|
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
|
+
)
|
|
6
15
|
|
|
7
16
|
# The key names a caller may write, and what the engine calls them.
|
|
8
17
|
const KEY_NAMES: Dictionary = {
|
|
@@ -108,7 +117,13 @@ func _init(p_log: GdhLog) -> void:
|
|
|
108
117
|
func add_input_action(params: Dictionary) -> Dictionary:
|
|
109
118
|
var action_name: String = str(params.get("action_name", ""))
|
|
110
119
|
var events: Array = params.get("events", [])
|
|
111
|
-
var
|
|
120
|
+
var setting: String = "input/" + action_name
|
|
121
|
+
var held: Variant = ProjectSettings.get_setting(setting) if ProjectSettings.has_setting(setting) else null
|
|
122
|
+
var old: Dictionary = held if held is Dictionary else {}
|
|
123
|
+
var old_deadzone: float = GdhRead.as_float(old.get("deadzone", 0.5), 0.5)
|
|
124
|
+
# A rebind that names no deadzone keeps the one the action has, which a caller adding a key did
|
|
125
|
+
# not mean to change.
|
|
126
|
+
var deadzone: float = GdhRead.as_float(params.get("deadzone", old_deadzone), old_deadzone)
|
|
112
127
|
|
|
113
128
|
_log.info("Adding input action: " + action_name)
|
|
114
129
|
# The editor's Input Map refuses these, since project.godot keys an action by its name and
|
|
@@ -139,47 +154,11 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
139
154
|
if not event is Dictionary:
|
|
140
155
|
return _log.failure("Every event must be an object, not " + JSON.stringify(event))
|
|
141
156
|
var fields: Dictionary = event
|
|
142
|
-
var
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if keycode.is_empty():
|
|
148
|
-
return _log.failure("A key event needs keycode")
|
|
149
|
-
var key_value: int = _keycode_value(keycode)
|
|
150
|
-
if key_value == KEY_NONE:
|
|
151
|
-
return _log.failure(_unknown_key(keycode))
|
|
152
|
-
|
|
153
|
-
var event_config: Dictionary = {"class_name": "InputEventKey", "keycode": key_value}
|
|
154
|
-
if fields.get("ctrl", false):
|
|
155
|
-
event_config["ctrl_pressed"] = true
|
|
156
|
-
if fields.get("alt", false):
|
|
157
|
-
event_config["alt_pressed"] = true
|
|
158
|
-
if fields.get("shift", false):
|
|
159
|
-
event_config["shift_pressed"] = true
|
|
160
|
-
events_config.append(event_config)
|
|
161
|
-
|
|
162
|
-
"mouse_button":
|
|
163
|
-
events_config.append(
|
|
164
|
-
{"class_name": "InputEventMouseButton", "button_index": fields.get("button", 1)}
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
"joypad_button":
|
|
168
|
-
events_config.append(
|
|
169
|
-
{"class_name": "InputEventJoypadButton", "button_index": fields.get("button", 0)}
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
"joypad_axis":
|
|
173
|
-
var motion: Dictionary = {
|
|
174
|
-
"class_name": "InputEventJoypadMotion",
|
|
175
|
-
"axis": fields.get("axis", 0),
|
|
176
|
-
"axis_value": fields.get("axisValue", 1),
|
|
177
|
-
}
|
|
178
|
-
events_config.append(motion)
|
|
179
|
-
|
|
180
|
-
_:
|
|
181
|
-
var known: String = "key, mouse_button, joypad_button, joypad_axis"
|
|
182
|
-
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)
|
|
183
162
|
|
|
184
163
|
if events_config.is_empty():
|
|
185
164
|
return _log.failure("events must hold at least one event")
|
|
@@ -190,7 +169,6 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
190
169
|
|
|
191
170
|
# An action that exists is rebound, which is how one of the engine's ui_ actions is changed, so
|
|
192
171
|
# what it held is answered rather than lost without a word.
|
|
193
|
-
var setting: String = "input/" + action_name
|
|
194
172
|
var answer: Dictionary = {
|
|
195
173
|
"action_name": action_name,
|
|
196
174
|
"events_count": events_config.size(),
|
|
@@ -199,8 +177,6 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
199
177
|
"was_new": not ProjectSettings.has_setting(setting),
|
|
200
178
|
}
|
|
201
179
|
if not answer["was_new"]:
|
|
202
|
-
var held: Variant = ProjectSettings.get_setting(setting)
|
|
203
|
-
var old: Dictionary = held if held is Dictionary else {}
|
|
204
180
|
var old_events: Array[Dictionary] = []
|
|
205
181
|
var listed: Variant = old.get("events", [])
|
|
206
182
|
if listed is Array:
|
|
@@ -210,7 +186,7 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
210
186
|
var bound: InputEvent = event
|
|
211
187
|
old_events.append(_shaped(bound))
|
|
212
188
|
answer["old_events"] = old_events
|
|
213
|
-
answer["old_deadzone"] =
|
|
189
|
+
answer["old_deadzone"] = old_deadzone
|
|
214
190
|
|
|
215
191
|
# Through ProjectSettings so the file is saved the way the editor saves it, header and
|
|
216
192
|
# every other line kept; a ConfigFile of project.godot drops the comments on the way out.
|
|
@@ -226,9 +202,12 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
226
202
|
## under text, which also covers a kind of event that form has no fields for.
|
|
227
203
|
static func _shaped(event: InputEvent) -> Dictionary:
|
|
228
204
|
var shaped: Dictionary = {"class_name": event.get_class(), "text": event.as_text()}
|
|
205
|
+
if event.device != ALL_DEVICES:
|
|
206
|
+
shaped["device"] = event.device
|
|
229
207
|
if event is InputEventKey:
|
|
230
208
|
var key: InputEventKey = event
|
|
231
|
-
|
|
209
|
+
if key.keycode != KEY_NONE or key.physical_keycode == KEY_NONE:
|
|
210
|
+
shaped["keycode"] = key.keycode
|
|
232
211
|
if key.physical_keycode != KEY_NONE:
|
|
233
212
|
shaped["physical_keycode"] = key.physical_keycode
|
|
234
213
|
for modifier: String in ["ctrl_pressed", "alt_pressed", "shift_pressed"]:
|
|
@@ -293,14 +272,28 @@ func remove_input_action(params: Dictionary) -> Dictionary:
|
|
|
293
272
|
return _log.failure("This project has no input action named " + action_name)
|
|
294
273
|
# The engine registers its own ui_ actions with a default, so clearing one from project.godot
|
|
295
274
|
# puts the engine's binding back rather than removing the action.
|
|
296
|
-
|
|
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
|
+
)
|
|
297
290
|
return _log.failure(
|
|
298
291
|
(
|
|
299
292
|
(
|
|
300
|
-
"%s is one of the engine's own actions
|
|
301
|
-
+ "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."
|
|
302
295
|
)
|
|
303
|
-
% action_name
|
|
296
|
+
% [action_name, setting]
|
|
304
297
|
)
|
|
305
298
|
)
|
|
306
299
|
ProjectSettings.set_setting(setting, null)
|
|
@@ -328,6 +321,7 @@ func build_input_action(deadzone: float, events: Array) -> Dictionary:
|
|
|
328
321
|
"InputEventKey":
|
|
329
322
|
var key: InputEventKey = InputEventKey.new()
|
|
330
323
|
key.keycode = GdhRead.as_int(event.get("keycode", 0)) as Key
|
|
324
|
+
key.physical_keycode = GdhRead.as_int(event.get("physical_keycode", 0)) as Key
|
|
331
325
|
key.ctrl_pressed = GdhRead.as_bool(event.get("ctrl_pressed", false))
|
|
332
326
|
key.alt_pressed = GdhRead.as_bool(event.get("alt_pressed", false))
|
|
333
327
|
key.shift_pressed = GdhRead.as_bool(event.get("shift_pressed", false))
|
|
@@ -354,6 +348,10 @@ func build_input_action(deadzone: float, events: Array) -> Dictionary:
|
|
|
354
348
|
|
|
355
349
|
_:
|
|
356
350
|
return _log.failure("Unknown input event class: " + evt_class)
|
|
351
|
+
# Every device, as the editor's Input Map binds an event: a new event names one device, and
|
|
352
|
+
# an action bound to it ignored a second joypad.
|
|
353
|
+
var bound: InputEvent = built.back()
|
|
354
|
+
bound.device = ALL_DEVICES
|
|
357
355
|
|
|
358
356
|
return {"deadzone": deadzone, "events": built}
|
|
359
357
|
|
|
@@ -361,6 +359,84 @@ func build_input_action(deadzone: float, events: Array) -> Dictionary:
|
|
|
361
359
|
# The keycode for a key name as the tool spells it, or KEY_NONE for a name the table lacks.
|
|
362
360
|
## The refusal for [param key_name], which names no key: the names nearest it, then every name taken,
|
|
363
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
|
+
|
|
364
440
|
static func _unknown_key(key_name: String) -> String:
|
|
365
441
|
var every: PackedStringArray = []
|
|
366
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,89 @@ 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
|
+
|
|
170
|
+
# Takes a setting's line out of project.godot, so the engine's default holds again, or nothing at all
|
|
171
|
+
# for one the engine does not declare: the way back from a set, which writes a misspelt name as given.
|
|
172
|
+
func unset_project_setting(params: Dictionary) -> Dictionary:
|
|
173
|
+
var setting_path: String = str(params.get("setting", ""))
|
|
174
|
+
if setting_path.is_empty():
|
|
175
|
+
return _log.failure("setting is required")
|
|
176
|
+
var default_value: Variant = ProjectSettings.property_get_revert(setting_path)
|
|
177
|
+
var held: bool = ProjectSettings.has_setting(setting_path)
|
|
178
|
+
var value: Variant = ProjectSettings.get_setting(setting_path) if held else null
|
|
179
|
+
var answer: Dictionary = {
|
|
180
|
+
"setting_path": setting_path,
|
|
181
|
+
"default_value": _values.serialize_value(default_value) if default_value != null else null,
|
|
182
|
+
}
|
|
183
|
+
# A setting at the engine's default has no line to take out, as the engine leaves it out of the file.
|
|
184
|
+
if not held or (default_value != null and value == default_value):
|
|
185
|
+
answer["removed"] = false
|
|
186
|
+
answer["saved"] = false
|
|
187
|
+
answer["note"] = "project.godot does not set %s, so there was nothing to take out." % setting_path
|
|
188
|
+
return _with_what_holds(answer, setting_path)
|
|
189
|
+
answer["old_value"] = _values.serialize_value(value)
|
|
190
|
+
# The default rather than null for a setting the engine declares: null takes the declaration away
|
|
191
|
+
# as well, and a value at its default is left out of the file all the same.
|
|
192
|
+
ProjectSettings.set_setting(setting_path, default_value)
|
|
193
|
+
var err: Error = GdhProjectFile.save()
|
|
194
|
+
if err != OK:
|
|
195
|
+
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
196
|
+
answer["removed"] = true
|
|
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]
|
|
211
|
+
return answer
|
|
212
|
+
|
|
213
|
+
|
|
169
214
|
# The settings in the same section as [param setting_path] named nearest it, for a new one that is
|
|
170
215
|
# likely a misspelt one: a setting nothing declares is written all the same, and is never read.
|
|
171
216
|
static func _settings_near(setting_path: String) -> PackedStringArray:
|
|
@@ -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",
|