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