gdharness 1.1.21 → 1.1.22
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 +1036 -525
- package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
- package/build/godot/addons/gdharness_editor/reading.gd +17 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
- package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
- package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +8 -4
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +28 -8
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +7 -299
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +20 -9
- package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
- package/build/godot/operations/audio_buses.gd +47 -11
- package/build/godot/operations/class_cache.gd +18 -2
- package/build/godot/operations/dependencies.gd +128 -17
- package/build/godot/operations/file_walk.gd +20 -0
- package/build/godot/operations/gdscript_analysis.gd +59 -20
- package/build/godot/operations/gdscript_authoring.gd +127 -60
- package/build/godot/operations/godot_operations.gd +24 -5
- package/build/godot/operations/import_pipeline.gd +179 -19
- package/build/godot/operations/logger.gd +2 -2
- package/build/godot/operations/plugins.gd +88 -70
- package/build/godot/operations/project_config.gd +41 -6
- package/build/godot/operations/project_diagnostics.gd +25 -10
- package/build/godot/operations/reading.gd +17 -0
- package/build/godot/operations/resource_files.gd +60 -17
- package/build/godot/operations/serialisation.gd +116 -7
- package/build/index.js +949 -435
- package/package.json +1 -1
|
@@ -52,16 +52,30 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
52
52
|
# answer: a `var x: int = 5 # note` carried `5 # note` as its default, and correcting the
|
|
53
53
|
# end of a parameter list put the comment into the return type, where `rfind(")")` had been
|
|
54
54
|
# hiding it by stopping inside the comment instead.
|
|
55
|
-
var
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
var raw: String = lines[i]
|
|
56
|
+
# Inside a string the raw line is counted, because a `#` in it is text and taking it for a
|
|
57
|
+
# comment would cut off the quotes that close the string.
|
|
58
|
+
if in_multiline_string:
|
|
59
|
+
if _triple_quotes(raw) % 2 == 1:
|
|
60
|
+
in_multiline_string = false
|
|
58
61
|
continue
|
|
62
|
+
var stripped: String = _without_comment(raw)
|
|
59
63
|
|
|
60
|
-
if
|
|
61
|
-
in_multiline_string = not in_multiline_string
|
|
64
|
+
if stripped.is_empty():
|
|
62
65
|
continue
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
# An odd count opens a string that runs on; an even one is a string that closed on the line
|
|
68
|
+
# it opened on. Toggling on any count lost `const HELP := """Usage"""`, and everything after
|
|
69
|
+
# it up to the next triple quote, with the state inverted from there to the end of the file.
|
|
70
|
+
# The line that opens one is still read: it can be the declaration the string belongs to.
|
|
71
|
+
if _triple_quotes(stripped) % 2 == 1:
|
|
72
|
+
in_multiline_string = true
|
|
73
|
+
|
|
74
|
+
# Only a line at the left margin declares one of the script's own members. Indented, it is a
|
|
75
|
+
# function's local or an inner class's member, and each was answered as the script's own:
|
|
76
|
+
# every `var` in every body, and an inner class's `extends` taken for the script's base.
|
|
77
|
+
if raw.begins_with(" ") or raw.begins_with("\t"):
|
|
78
|
+
_collect_dependencies(stripped, dependencies)
|
|
65
79
|
continue
|
|
66
80
|
|
|
67
81
|
# Annotations may share the line with anything they annotate, so every branch below decides
|
|
@@ -116,14 +130,11 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
116
130
|
elif header.begins_with("class "):
|
|
117
131
|
inner_classes.append(header.substr(6).split(":")[0].split(" ")[0].strip_edges())
|
|
118
132
|
|
|
119
|
-
|
|
120
|
-
for dep: String in _extract_dependencies(stripped):
|
|
121
|
-
if dep not in dependencies:
|
|
122
|
-
dependencies.append(dep)
|
|
133
|
+
_collect_dependencies(stripped, dependencies)
|
|
123
134
|
|
|
124
135
|
# After the loop, because it needs the base the loop reads, and a file that declared one after
|
|
125
136
|
# its first function would otherwise be judged against the default.
|
|
126
|
-
_mark_virtuals(functions, extends_name)
|
|
137
|
+
_mark_virtuals(functions, extends_name, full_script_path)
|
|
127
138
|
|
|
128
139
|
var answer: Dictionary = {
|
|
129
140
|
"path": script_path,
|
|
@@ -151,8 +162,8 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
151
162
|
# with a private helper in it. The virtuals are the ones the native ancestor declares with
|
|
152
163
|
# `METHOD_FLAG_VIRTUAL`, which is a question `ClassDB` answers exactly, and `class_has_method` does
|
|
153
164
|
# not: it says false for `_ready` on `Node`, so the method list is what has to be walked.
|
|
154
|
-
func _mark_virtuals(functions: Array[Dictionary], extends_name: String) -> void:
|
|
155
|
-
var native: String = _native_ancestor(extends_name)
|
|
165
|
+
func _mark_virtuals(functions: Array[Dictionary], extends_name: String, from_path: String) -> void:
|
|
166
|
+
var native: String = _native_ancestor(extends_name, from_path)
|
|
156
167
|
var virtuals: Dictionary = {}
|
|
157
168
|
if not native.is_empty():
|
|
158
169
|
for m: Dictionary in ClassDB.class_get_method_list(native, false):
|
|
@@ -168,11 +179,12 @@ func _mark_virtuals(functions: Array[Dictionary], extends_name: String) -> void:
|
|
|
168
179
|
# A base named by a script is followed; a base that is neither a script nor a class the engine knows
|
|
169
180
|
# leaves nothing to ask, and then no function is called virtual rather than every underscore being
|
|
170
181
|
# guessed at.
|
|
171
|
-
func _native_ancestor(base: String) -> String:
|
|
182
|
+
func _native_ancestor(base: String, from_path: String) -> String:
|
|
172
183
|
var seen: Array[String] = []
|
|
173
184
|
var current: String = base
|
|
185
|
+
var declared_in: String = from_path
|
|
174
186
|
while true:
|
|
175
|
-
var path: String = _script_named(current)
|
|
187
|
+
var path: String = _script_named(current, declared_in)
|
|
176
188
|
if path.is_empty():
|
|
177
189
|
break
|
|
178
190
|
if path in seen:
|
|
@@ -182,6 +194,7 @@ func _native_ancestor(base: String) -> String:
|
|
|
182
194
|
if above.is_empty():
|
|
183
195
|
return ""
|
|
184
196
|
current = str(above.get("extends", ""))
|
|
197
|
+
declared_in = path
|
|
185
198
|
return current if ClassDB.class_exists(current) else ""
|
|
186
199
|
|
|
187
200
|
|
|
@@ -196,8 +209,9 @@ func _native_ancestor(base: String) -> String:
|
|
|
196
209
|
func _add_inherited(answer: Dictionary, seen: Array[String]) -> void:
|
|
197
210
|
var inherits_from: Array[String] = []
|
|
198
211
|
var base: String = str(answer.get("extends", ""))
|
|
212
|
+
var declared_in: String = str(answer.get("full_path", ""))
|
|
199
213
|
while true:
|
|
200
|
-
var base_path: String = _script_named(base)
|
|
214
|
+
var base_path: String = _script_named(base, declared_in)
|
|
201
215
|
if base_path.is_empty() or base_path in seen:
|
|
202
216
|
break
|
|
203
217
|
seen.append(base_path)
|
|
@@ -216,16 +230,28 @@ func _add_inherited(answer: Dictionary, seen: Array[String]) -> void:
|
|
|
216
230
|
carried["inherited_from"] = base_path
|
|
217
231
|
mine.append(carried)
|
|
218
232
|
base = str(above.get("extends", ""))
|
|
233
|
+
declared_in = base_path
|
|
219
234
|
answer["inherits_from"] = inherits_from
|
|
220
235
|
|
|
221
236
|
|
|
222
237
|
# The file a base names, whether it named a path or a class, or empty for a native class.
|
|
223
|
-
|
|
238
|
+
#
|
|
239
|
+
# A quoted path is resolved the way the engine resolves it: `extends "base.gd"` is relative to the
|
|
240
|
+
# script [param declared_in], and asking whether `base.gd` exists asked about nothing, so such a
|
|
241
|
+
# script had no ancestor and none of its engine callbacks were virtual.
|
|
242
|
+
func _script_named(base: String, declared_in: String) -> String:
|
|
224
243
|
if base.is_empty():
|
|
225
244
|
return ""
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
245
|
+
var quoted: String = ""
|
|
246
|
+
if base.length() >= 2 and (base[0] == '"' or base[0] == "'") and base.ends_with(base[0]):
|
|
247
|
+
quoted = base.substr(1, base.length() - 2)
|
|
248
|
+
if not quoted.is_empty():
|
|
249
|
+
if quoted.begins_with("uid://"):
|
|
250
|
+
var id: int = ResourceUID.text_to_id(quoted)
|
|
251
|
+
quoted = ResourceUID.get_id_path(id) if ResourceUID.has_id(id) else ""
|
|
252
|
+
elif not quoted.begins_with("res://"):
|
|
253
|
+
quoted = declared_in.get_base_dir().path_join(quoted).simplify_path()
|
|
254
|
+
return quoted if not quoted.is_empty() and FileAccess.file_exists(quoted) else ""
|
|
229
255
|
for entry: Dictionary in ProjectSettings.get_global_class_list():
|
|
230
256
|
if str(entry.get("class", "")) == base:
|
|
231
257
|
var path: String = str(entry.get("path", ""))
|
|
@@ -562,6 +588,19 @@ func _parse_param(param_text: String) -> Dictionary:
|
|
|
562
588
|
return {"name": name, "type": type_hint, "default": default_value, "is_rest": is_rest}
|
|
563
589
|
|
|
564
590
|
|
|
591
|
+
func _collect_dependencies(line: String, into: Array[String]) -> void:
|
|
592
|
+
if "load(" not in line:
|
|
593
|
+
return
|
|
594
|
+
for dep: String in _extract_dependencies(line):
|
|
595
|
+
if dep not in into:
|
|
596
|
+
into.append(dep)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
# How many triple quotes of either kind [param line] holds.
|
|
600
|
+
static func _triple_quotes(line: String) -> int:
|
|
601
|
+
return line.count('"""') + line.count("'''")
|
|
602
|
+
|
|
603
|
+
|
|
565
604
|
func _extract_dependencies(line: String) -> Array[String]:
|
|
566
605
|
var deps: Array[String] = []
|
|
567
606
|
var regex: RegEx = Patterns.compiled("(?:preload|load)\\s*\\(\\s*[\"']([^\"']+)[\"']\\s*\\)")
|
|
@@ -23,6 +23,24 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
23
23
|
|
|
24
24
|
_log.info("Creating GDScript: " + script_path)
|
|
25
25
|
|
|
26
|
+
# Content is the whole file, so a header written above it would be a second one: a content that
|
|
27
|
+
# starts with its own `extends` did not parse, and one without it was not the file that was sent.
|
|
28
|
+
if not content.is_empty():
|
|
29
|
+
var alongside: Array[String] = []
|
|
30
|
+
for name: String in ["class_name", "extends", "template"]:
|
|
31
|
+
if not str(params.get(name, "")).is_empty():
|
|
32
|
+
alongside.append(name)
|
|
33
|
+
if not alongside.is_empty():
|
|
34
|
+
return _log.failure(
|
|
35
|
+
(
|
|
36
|
+
"content is the whole file, so "
|
|
37
|
+
+ " and ".join(alongside)
|
|
38
|
+
+ " cannot be given with it: write "
|
|
39
|
+
+ ("them" if alongside.size() > 1 else "it")
|
|
40
|
+
+ " into content instead"
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
26
44
|
var full_script_path: String = script_path
|
|
27
45
|
if not full_script_path.begins_with("res://"):
|
|
28
46
|
full_script_path = "res://" + full_script_path
|
|
@@ -41,20 +59,16 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
41
59
|
if error != OK:
|
|
42
60
|
return _log.failure("Failed to create directory: " + script_dir + ", error: " + str(error))
|
|
43
61
|
|
|
44
|
-
var script_content: String =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
script_content += content
|
|
55
|
-
else:
|
|
56
|
-
script_content += "func _ready() -> void:\n"
|
|
57
|
-
script_content += "\tpass\n"
|
|
62
|
+
var script_content: String = content
|
|
63
|
+
if content.is_empty():
|
|
64
|
+
if not cls_name_param.is_empty():
|
|
65
|
+
script_content += "class_name " + cls_name_param + "\n"
|
|
66
|
+
script_content += "extends " + extends_class + "\n\n"
|
|
67
|
+
if not template.is_empty():
|
|
68
|
+
script_content += _script_template(template)
|
|
69
|
+
else:
|
|
70
|
+
script_content += "func _ready() -> void:\n"
|
|
71
|
+
script_content += "\tpass\n"
|
|
58
72
|
|
|
59
73
|
var file: FileAccess = FileAccess.open(full_script_path, FileAccess.WRITE)
|
|
60
74
|
if not file:
|
|
@@ -65,21 +79,20 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
65
79
|
if not stored:
|
|
66
80
|
return _log.failure("Failed to write to script file: " + full_script_path)
|
|
67
81
|
|
|
68
|
-
# Whether the engine accepts what was written, parsed under this project's own warning
|
|
69
|
-
# settings: a script that does not load is a script the caller wants to hear about now,
|
|
70
|
-
# and the reason is on stderr, which comes back with the answer.
|
|
71
82
|
var written: Script = ResourceLoader.load(full_script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
|
|
72
|
-
|
|
73
|
-
return {
|
|
83
|
+
var answer: Dictionary = {
|
|
74
84
|
"success": true,
|
|
75
85
|
"script_path": script_path,
|
|
76
86
|
"full_path": full_script_path,
|
|
77
87
|
"absolute_path": ProjectSettings.globalize_path(full_script_path),
|
|
78
|
-
"registered": not cls_name_param.is_empty(),
|
|
79
|
-
"extends": extends_class,
|
|
80
88
|
"template_used": template if not template.is_empty() else "none",
|
|
81
|
-
"parses": written
|
|
89
|
+
"parses": _parses(written),
|
|
82
90
|
}
|
|
91
|
+
# Read off the file as the engine parsed it rather than off the arguments, which content leaves out.
|
|
92
|
+
if written != null:
|
|
93
|
+
answer["extends"] = _base_named(written)
|
|
94
|
+
answer["class_name"] = str(written.get_global_name())
|
|
95
|
+
return answer
|
|
83
96
|
|
|
84
97
|
|
|
85
98
|
# Modify an existing GDScript file by adding functions, variables, or signals
|
|
@@ -103,34 +116,40 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
103
116
|
var original_content: String = file.get_as_text()
|
|
104
117
|
file.close()
|
|
105
118
|
|
|
119
|
+
# All of them checked before any is made, so a call is made whole or not at all: one skipped for a
|
|
120
|
+
# missing name used to leave the rest written and the answer saying success over an empty list.
|
|
121
|
+
var problems: Array[String] = _modification_problems(modifications)
|
|
122
|
+
if not problems.is_empty():
|
|
123
|
+
return _log.failure("Nothing was changed: " + "; ".join(problems))
|
|
124
|
+
|
|
106
125
|
var lines: Array[String] = []
|
|
107
126
|
lines.assign(original_content.split("\n"))
|
|
108
|
-
|
|
127
|
+
# Each placed line as a zero-based index that moves down whenever a later insertion lands above
|
|
128
|
+
# it, so the numbers answered are where the lines are in the file written, not where each went
|
|
129
|
+
# at the moment it was placed.
|
|
130
|
+
var placed: Array[Dictionary] = []
|
|
109
131
|
|
|
110
132
|
for mod: Variant in modifications:
|
|
111
|
-
if not mod is Dictionary:
|
|
112
|
-
_log.error("Modification must be an object")
|
|
113
|
-
continue
|
|
114
133
|
var fields: Dictionary = mod
|
|
115
134
|
var mod_type: String = str(fields.get("type", ""))
|
|
116
|
-
var
|
|
117
|
-
|
|
118
|
-
if mod_name.is_empty():
|
|
119
|
-
_log.error("Modification missing 'name' field")
|
|
120
|
-
continue
|
|
121
|
-
|
|
135
|
+
var at: int = -1
|
|
122
136
|
match mod_type:
|
|
123
137
|
"add_variable":
|
|
124
|
-
|
|
125
|
-
modifications_applied.append({"type": "add_variable", "name": mod_name, "line": line})
|
|
138
|
+
at = _add_variable(lines, fields, placed)
|
|
126
139
|
"add_signal":
|
|
127
|
-
|
|
128
|
-
modifications_applied.append({"type": "add_signal", "name": mod_name, "line": line})
|
|
140
|
+
at = _add_signal(lines, fields, placed)
|
|
129
141
|
"add_function":
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
at = _add_function(lines, fields, placed)
|
|
143
|
+
if at == -1:
|
|
144
|
+
return _log.failure(
|
|
145
|
+
"Nothing was changed: " + mod_type + " " + str(fields.get("name")) + " could not be placed"
|
|
146
|
+
)
|
|
147
|
+
placed.append({"type": mod_type, "name": str(fields.get("name")), "index": at})
|
|
148
|
+
|
|
149
|
+
var modifications_applied: Array[Dictionary] = []
|
|
150
|
+
for entry: Dictionary in placed:
|
|
151
|
+
var index: int = entry["index"]
|
|
152
|
+
modifications_applied.append({"type": entry["type"], "name": entry["name"], "line": index + 1})
|
|
134
153
|
|
|
135
154
|
file = FileAccess.open(full_script_path, FileAccess.WRITE)
|
|
136
155
|
if not file:
|
|
@@ -141,17 +160,75 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
141
160
|
if not rewrote:
|
|
142
161
|
return _log.failure("Failed to write to script file: " + full_script_path)
|
|
143
162
|
|
|
163
|
+
var made: Script = ResourceLoader.load(full_script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
|
|
144
164
|
return {
|
|
145
165
|
"success": true,
|
|
146
166
|
"script_path": script_path,
|
|
147
167
|
"modifications_applied": modifications_applied,
|
|
148
|
-
"total_modifications": modifications_applied.size()
|
|
168
|
+
"total_modifications": modifications_applied.size(),
|
|
169
|
+
"parses": _parses(made),
|
|
149
170
|
}
|
|
150
171
|
|
|
151
172
|
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
173
|
+
# Every reason [param modifications] cannot be made as asked, each naming the entry by position.
|
|
174
|
+
static func _modification_problems(modifications: Array) -> Array[String]:
|
|
175
|
+
var problems: Array[String] = []
|
|
176
|
+
if modifications.is_empty():
|
|
177
|
+
problems.append("modifications is empty")
|
|
178
|
+
for index: int in range(modifications.size()):
|
|
179
|
+
var mod: Variant = modifications[index]
|
|
180
|
+
var which: String = "modification " + str(index + 1)
|
|
181
|
+
if not mod is Dictionary:
|
|
182
|
+
problems.append(which + " is not an object")
|
|
183
|
+
continue
|
|
184
|
+
var fields: Dictionary = mod
|
|
185
|
+
var mod_type: String = str(fields.get("type", ""))
|
|
186
|
+
var mod_name: String = str(fields.get("name", ""))
|
|
187
|
+
if mod_type not in ["add_variable", "add_signal", "add_function"]:
|
|
188
|
+
problems.append(
|
|
189
|
+
which + " has type '" + mod_type + "', not add_variable, add_signal or add_function"
|
|
190
|
+
)
|
|
191
|
+
if mod_name.is_empty():
|
|
192
|
+
problems.append(which + " has no name")
|
|
193
|
+
elif not mod_name.is_valid_unicode_identifier():
|
|
194
|
+
problems.append(which + " is named '" + mod_name + "', which GDScript does not accept as a name")
|
|
195
|
+
return problems
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# Inserts [param new_lines] at [param at] and moves every placed line at or below it down by as many.
|
|
199
|
+
static func _insert(
|
|
200
|
+
lines: Array[String], at: int, new_lines: Array[String], placed: Array[Dictionary]
|
|
201
|
+
) -> bool:
|
|
202
|
+
for i: int in range(new_lines.size() - 1, -1, -1):
|
|
203
|
+
if lines.insert(at, new_lines[i]) != OK:
|
|
204
|
+
push_error("gdharness: could not place a line at " + str(at))
|
|
205
|
+
return false
|
|
206
|
+
for entry: Dictionary in placed:
|
|
207
|
+
var index: int = entry["index"]
|
|
208
|
+
if index >= at:
|
|
209
|
+
entry["index"] = index + new_lines.size()
|
|
210
|
+
return true
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# Whether the engine accepts [param script], parsed under this project's own warning settings: a
|
|
214
|
+
# script that does not load is one the caller wants to hear about now, and the reason is on stderr,
|
|
215
|
+
# which comes back with the answer. A script that fails to parse still loads, as one that cannot be
|
|
216
|
+
# instantiated; an abstract one that parses can be, on 4.7.
|
|
217
|
+
static func _parses(script: Script) -> bool:
|
|
218
|
+
return script != null and script.can_instantiate()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# What [param script] extends: the base script by class name or path, or the native class.
|
|
222
|
+
static func _base_named(script: Script) -> String:
|
|
223
|
+
var base: Script = script.get_base_script()
|
|
224
|
+
if base == null:
|
|
225
|
+
return str(script.get_instance_base_type())
|
|
226
|
+
var named: String = str(base.get_global_name())
|
|
227
|
+
return named if not named.is_empty() else base.resource_path
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# Inserts the declaration and answers with its zero-based index, or -1 for one that could not be placed.
|
|
231
|
+
func _add_variable(lines: Array[String], mod: Dictionary, placed: Array[Dictionary]) -> int:
|
|
155
232
|
var var_name: String = str(mod.get("name", ""))
|
|
156
233
|
var var_type: String = str(mod.get("varType", ""))
|
|
157
234
|
var default_value: String = str(mod.get("defaultValue", ""))
|
|
@@ -186,10 +263,7 @@ func _add_variable(lines: Array[String], mod: Dictionary) -> int:
|
|
|
186
263
|
var_line += ": Variant"
|
|
187
264
|
|
|
188
265
|
var insert_line: int = _variable_insertion_point(lines)
|
|
189
|
-
if lines
|
|
190
|
-
push_error("gdharness: could not place a declaration at line " + str(insert_line))
|
|
191
|
-
return 0
|
|
192
|
-
return insert_line + 1
|
|
266
|
+
return insert_line if _insert(lines, insert_line, [var_line], placed) else -1
|
|
193
267
|
|
|
194
268
|
|
|
195
269
|
# The type name a constant expression evaluates to, or Variant when it is not one: a call
|
|
@@ -208,7 +282,7 @@ func _type_of_literal(expression: String) -> String:
|
|
|
208
282
|
return type_string(typeof(value))
|
|
209
283
|
|
|
210
284
|
|
|
211
|
-
func _add_signal(lines: Array[String], mod: Dictionary) -> int:
|
|
285
|
+
func _add_signal(lines: Array[String], mod: Dictionary, placed: Array[Dictionary]) -> int:
|
|
212
286
|
var signal_name: String = str(mod.get("name", ""))
|
|
213
287
|
var signal_params: String = str(mod.get("params", ""))
|
|
214
288
|
|
|
@@ -217,13 +291,11 @@ func _add_signal(lines: Array[String], mod: Dictionary) -> int:
|
|
|
217
291
|
signal_line += "(" + signal_params + ")"
|
|
218
292
|
|
|
219
293
|
var insert_line: int = _signal_insertion_point(lines)
|
|
220
|
-
if lines
|
|
221
|
-
push_error("gdharness: could not place a signal at line " + str(insert_line))
|
|
222
|
-
return 0
|
|
223
|
-
return insert_line + 1
|
|
294
|
+
return insert_line if _insert(lines, insert_line, [signal_line], placed) else -1
|
|
224
295
|
|
|
225
296
|
|
|
226
|
-
func
|
|
297
|
+
# Answers with the zero-based index of the `func` line, below the blank line that separates it.
|
|
298
|
+
func _add_function(lines: Array[String], mod: Dictionary, placed: Array[Dictionary]) -> int:
|
|
227
299
|
var func_name: String = str(mod.get("name", ""))
|
|
228
300
|
var func_params: String = str(mod.get("params", ""))
|
|
229
301
|
# A function that names no return type returns nothing, and says so, for the same reason
|
|
@@ -248,12 +320,7 @@ func _add_function(lines: Array[String], mod: Dictionary) -> int:
|
|
|
248
320
|
if insert_line < lines.size():
|
|
249
321
|
func_lines.append("")
|
|
250
322
|
|
|
251
|
-
|
|
252
|
-
if lines.insert(insert_line, func_lines[i]) != OK:
|
|
253
|
-
push_error("gdharness: could not place a function at line " + str(insert_line))
|
|
254
|
-
return 0
|
|
255
|
-
|
|
256
|
-
return insert_line + 1
|
|
323
|
+
return insert_line + 1 if _insert(lines, insert_line, func_lines, placed) else -1
|
|
257
324
|
|
|
258
325
|
|
|
259
326
|
func _variable_insertion_point(lines: Array[String]) -> int:
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
extends SceneTree
|
|
3
3
|
|
|
4
4
|
# The command line the server runs is `godot --headless --path <project> --script <this>
|
|
5
|
-
# <operation> @file:<params.json>`, and one JSON object
|
|
6
|
-
# operation does lives in a sibling module; this file is the
|
|
5
|
+
# <operation> @file:<params.json> <answer.json>`, and the one JSON object written to the answer
|
|
6
|
+
# file is the answer. Everything an operation does lives in a sibling module; this file is the
|
|
7
|
+
# command table and the wire format.
|
|
8
|
+
#
|
|
9
|
+
# A file rather than a line on stdout, because the project's autoloads run after `_init` and print
|
|
10
|
+
# into the same stream: an autoload printing a dictionary in `_process` was the last JSON line out
|
|
11
|
+
# and was taken as the answer. Nothing the project prints can reach the file.
|
|
7
12
|
#
|
|
8
13
|
# Each module is preloaded by a path relative to this script rather than a res:// one, because
|
|
9
14
|
# the operations directory ships inside the server package and is handed to the engine as an
|
|
@@ -39,8 +44,9 @@ func _init() -> void:
|
|
|
39
44
|
return
|
|
40
45
|
|
|
41
46
|
var params_index: int = script_index + 3
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
var answer_index: int = script_index + 4
|
|
48
|
+
if args.size() <= answer_index:
|
|
49
|
+
_log.error("Usage: godot_operations.gd <operation> @file:<params.json> <answer.json>")
|
|
44
50
|
_log.error("Not enough command-line arguments provided.")
|
|
45
51
|
quit(1)
|
|
46
52
|
return
|
|
@@ -65,7 +71,20 @@ func _init() -> void:
|
|
|
65
71
|
quit(1)
|
|
66
72
|
return
|
|
67
73
|
|
|
68
|
-
|
|
74
|
+
var answer_path: String = args[answer_index]
|
|
75
|
+
var answer: FileAccess = FileAccess.open(answer_path, FileAccess.WRITE)
|
|
76
|
+
if answer == null:
|
|
77
|
+
_log.error(
|
|
78
|
+
"Could not write the answer to " + answer_path + ": " + error_string(FileAccess.get_open_error())
|
|
79
|
+
)
|
|
80
|
+
quit(1)
|
|
81
|
+
return
|
|
82
|
+
var written: bool = answer.store_string(JSON.stringify(payload))
|
|
83
|
+
answer.close()
|
|
84
|
+
if not written:
|
|
85
|
+
_log.error("Could not write the answer to " + answer_path)
|
|
86
|
+
quit(1)
|
|
87
|
+
return
|
|
69
88
|
quit()
|
|
70
89
|
|
|
71
90
|
|