gdharness 0.4.1
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/LICENSE +22 -0
- package/README.md +125 -0
- package/build/cli.js +30353 -0
- package/build/godot/addons/auto_reload/auto_reload.gd +89 -0
- package/build/godot/addons/auto_reload/plugin.cfg +6 -0
- package/build/godot/addons/gdharness_editor/bridge_client.gd +197 -0
- package/build/godot/addons/gdharness_editor/plugin.cfg +6 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +88 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +104 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +397 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +85 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +427 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +885 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +292 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +77 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +254 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +283 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +169 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +119 -0
- package/build/godot/operations/audio_buses.gd +125 -0
- package/build/godot/operations/class_cache.gd +180 -0
- package/build/godot/operations/classdb_queries.gd +252 -0
- package/build/godot/operations/dependencies.gd +293 -0
- package/build/godot/operations/file_walk.gd +55 -0
- package/build/godot/operations/gdscript_analysis.gd +288 -0
- package/build/godot/operations/gdscript_authoring.gd +419 -0
- package/build/godot/operations/godot_operations.gd +186 -0
- package/build/godot/operations/import_pipeline.gd +390 -0
- package/build/godot/operations/input_actions.gd +237 -0
- package/build/godot/operations/logger.gd +33 -0
- package/build/godot/operations/plugins.gd +159 -0
- package/build/godot/operations/project_config.gd +153 -0
- package/build/godot/operations/project_diagnostics.gd +159 -0
- package/build/godot/operations/resource_files.gd +132 -0
- package/build/godot/operations/serialisation.gd +154 -0
- package/build/index.js +29252 -0
- package/package.json +57 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
const Log = preload("logger.gd")
|
|
4
|
+
|
|
5
|
+
var _log: Log
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
func _init(p_log: Log) -> void:
|
|
9
|
+
_log = p_log
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Create a new GDScript file with proper structure and optional templates
|
|
13
|
+
func create_gdscript(params: Dictionary) -> Dictionary:
|
|
14
|
+
var script_path: String = str(params.get("script_path", ""))
|
|
15
|
+
var cls_name_param: String = str(params.get("class_name", ""))
|
|
16
|
+
var extends_class: String = str(params.get("extends", "Node"))
|
|
17
|
+
if extends_class.is_empty():
|
|
18
|
+
extends_class = "Node"
|
|
19
|
+
var content: String = str(params.get("content", ""))
|
|
20
|
+
var template: String = str(params.get("template", ""))
|
|
21
|
+
|
|
22
|
+
_log.info("Creating GDScript: " + script_path)
|
|
23
|
+
|
|
24
|
+
var full_script_path: String = script_path
|
|
25
|
+
if not full_script_path.begins_with("res://"):
|
|
26
|
+
full_script_path = "res://" + full_script_path
|
|
27
|
+
|
|
28
|
+
if not full_script_path.ends_with(".gd"):
|
|
29
|
+
return _log.failure("Script path must end with .gd extension")
|
|
30
|
+
|
|
31
|
+
if FileAccess.file_exists(full_script_path):
|
|
32
|
+
return _log.failure("Script file already exists: " + full_script_path)
|
|
33
|
+
|
|
34
|
+
var dir: DirAccess = DirAccess.open("res://")
|
|
35
|
+
var script_dir: String = full_script_path.get_base_dir()
|
|
36
|
+
if script_dir != "res://" and not dir.dir_exists(script_dir.substr(6)):
|
|
37
|
+
_log.debug("Creating directory: " + script_dir)
|
|
38
|
+
var error: Error = dir.make_dir_recursive(script_dir.substr(6))
|
|
39
|
+
if error != OK:
|
|
40
|
+
return _log.failure("Failed to create directory: " + script_dir + ", error: " + str(error))
|
|
41
|
+
|
|
42
|
+
var script_content: String = ""
|
|
43
|
+
|
|
44
|
+
if not cls_name_param.is_empty():
|
|
45
|
+
script_content += "class_name " + cls_name_param + "\n"
|
|
46
|
+
|
|
47
|
+
script_content += "extends " + extends_class + "\n\n"
|
|
48
|
+
|
|
49
|
+
if not template.is_empty():
|
|
50
|
+
script_content += _script_template(template)
|
|
51
|
+
elif not content.is_empty():
|
|
52
|
+
script_content += content
|
|
53
|
+
else:
|
|
54
|
+
script_content += "func _ready() -> void:\n"
|
|
55
|
+
script_content += "\tpass\n"
|
|
56
|
+
|
|
57
|
+
var file: FileAccess = FileAccess.open(full_script_path, FileAccess.WRITE)
|
|
58
|
+
if not file:
|
|
59
|
+
return _log.failure("Failed to create script file: " + full_script_path)
|
|
60
|
+
|
|
61
|
+
file.store_string(script_content)
|
|
62
|
+
file.close()
|
|
63
|
+
|
|
64
|
+
# Whether the engine accepts what was written, parsed under this project's own warning
|
|
65
|
+
# settings: a script that does not load is a script the caller wants to hear about now,
|
|
66
|
+
# and the reason is on stderr, which comes back with the answer.
|
|
67
|
+
var written: Script = ResourceLoader.load(full_script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"success": true,
|
|
71
|
+
"script_path": script_path,
|
|
72
|
+
"full_path": full_script_path,
|
|
73
|
+
"absolute_path": ProjectSettings.globalize_path(full_script_path),
|
|
74
|
+
"registered": not cls_name_param.is_empty(),
|
|
75
|
+
"extends": extends_class,
|
|
76
|
+
"template_used": template if not template.is_empty() else "none",
|
|
77
|
+
"parses": written != null and written.can_instantiate(),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Modify an existing GDScript file by adding functions, variables, or signals
|
|
82
|
+
func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
83
|
+
var script_path: String = str(params.get("script_path", ""))
|
|
84
|
+
var modifications: Array = params.get("modifications", [])
|
|
85
|
+
|
|
86
|
+
_log.info("Modifying GDScript: " + script_path)
|
|
87
|
+
|
|
88
|
+
var full_script_path: String = script_path
|
|
89
|
+
if not full_script_path.begins_with("res://"):
|
|
90
|
+
full_script_path = "res://" + full_script_path
|
|
91
|
+
|
|
92
|
+
if not FileAccess.file_exists(full_script_path):
|
|
93
|
+
return _log.failure("Script file does not exist: " + full_script_path)
|
|
94
|
+
|
|
95
|
+
var file: FileAccess = FileAccess.open(full_script_path, FileAccess.READ)
|
|
96
|
+
if not file:
|
|
97
|
+
return _log.failure("Failed to open script file: " + full_script_path)
|
|
98
|
+
|
|
99
|
+
var original_content: String = file.get_as_text()
|
|
100
|
+
file.close()
|
|
101
|
+
|
|
102
|
+
var lines: Array[String] = []
|
|
103
|
+
lines.assign(original_content.split("\n"))
|
|
104
|
+
var modifications_applied: Array[Dictionary] = []
|
|
105
|
+
|
|
106
|
+
for mod: Variant in modifications:
|
|
107
|
+
if not mod is Dictionary:
|
|
108
|
+
_log.error("Modification must be an object")
|
|
109
|
+
continue
|
|
110
|
+
var fields: Dictionary = mod
|
|
111
|
+
var mod_type: String = str(fields.get("type", ""))
|
|
112
|
+
var mod_name: String = str(fields.get("name", ""))
|
|
113
|
+
|
|
114
|
+
if mod_name.is_empty():
|
|
115
|
+
_log.error("Modification missing 'name' field")
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
match mod_type:
|
|
119
|
+
"add_variable":
|
|
120
|
+
var line: int = _add_variable(lines, fields)
|
|
121
|
+
modifications_applied.append({"type": "add_variable", "name": mod_name, "line": line})
|
|
122
|
+
"add_signal":
|
|
123
|
+
var line: int = _add_signal(lines, fields)
|
|
124
|
+
modifications_applied.append({"type": "add_signal", "name": mod_name, "line": line})
|
|
125
|
+
"add_function":
|
|
126
|
+
var line: int = _add_function(lines, fields)
|
|
127
|
+
modifications_applied.append({"type": "add_function", "name": mod_name, "line": line})
|
|
128
|
+
_:
|
|
129
|
+
_log.error("Unknown modification type: " + mod_type)
|
|
130
|
+
|
|
131
|
+
file = FileAccess.open(full_script_path, FileAccess.WRITE)
|
|
132
|
+
if not file:
|
|
133
|
+
return _log.failure("Failed to write to script file: " + full_script_path)
|
|
134
|
+
|
|
135
|
+
file.store_string("\n".join(lines))
|
|
136
|
+
file.close()
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"success": true,
|
|
140
|
+
"script_path": script_path,
|
|
141
|
+
"modifications_applied": modifications_applied,
|
|
142
|
+
"total_modifications": modifications_applied.size()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Inserts the declaration and answers with its one-based line number.
|
|
147
|
+
func _add_variable(lines: Array[String], mod: Dictionary) -> int:
|
|
148
|
+
var var_name: String = str(mod.get("name", ""))
|
|
149
|
+
var var_type: String = str(mod.get("varType", ""))
|
|
150
|
+
var default_value: String = str(mod.get("defaultValue", ""))
|
|
151
|
+
var is_export: bool = bool(mod.get("isExport", false))
|
|
152
|
+
var export_hint: String = str(mod.get("exportHint", ""))
|
|
153
|
+
var is_onready: bool = bool(mod.get("isOnready", false))
|
|
154
|
+
|
|
155
|
+
var var_line: String = ""
|
|
156
|
+
|
|
157
|
+
if is_export:
|
|
158
|
+
if not export_hint.is_empty():
|
|
159
|
+
var_line += "@export_" + export_hint + " "
|
|
160
|
+
else:
|
|
161
|
+
var_line += "@export "
|
|
162
|
+
|
|
163
|
+
if is_onready:
|
|
164
|
+
var_line += "@onready "
|
|
165
|
+
|
|
166
|
+
var_line += "var " + var_name
|
|
167
|
+
|
|
168
|
+
# Every declaration this writes carries a type, so it parses in a project that treats an
|
|
169
|
+
# untyped or an inferred declaration as an error, which is the strictest setting Godot has.
|
|
170
|
+
# A value with no type gets the type the value evaluates to; a value nothing can evaluate
|
|
171
|
+
# here, and a declaration with neither, are spelled out as Variant.
|
|
172
|
+
if not var_type.is_empty():
|
|
173
|
+
var_line += ": " + var_type
|
|
174
|
+
if not default_value.is_empty():
|
|
175
|
+
var_line += " = " + default_value
|
|
176
|
+
elif not default_value.is_empty():
|
|
177
|
+
var_line += ": " + _type_of_literal(default_value) + " = " + default_value
|
|
178
|
+
else:
|
|
179
|
+
var_line += ": Variant"
|
|
180
|
+
|
|
181
|
+
var insert_line: int = _variable_insertion_point(lines)
|
|
182
|
+
lines.insert(insert_line, var_line)
|
|
183
|
+
return insert_line + 1
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# The type name a constant expression evaluates to, or Variant when it is not one: a call
|
|
187
|
+
# into the script's own scope cannot be evaluated from here, and a type guessed for it would
|
|
188
|
+
# be a claim the engine then refuses at load.
|
|
189
|
+
func _type_of_literal(expression: String) -> String:
|
|
190
|
+
var parser: Expression = Expression.new()
|
|
191
|
+
if parser.parse(expression) != OK:
|
|
192
|
+
return "Variant"
|
|
193
|
+
var value: Variant = parser.execute([], null, false)
|
|
194
|
+
if parser.has_execute_failed() or value == null:
|
|
195
|
+
return "Variant"
|
|
196
|
+
if value is Object:
|
|
197
|
+
var object: Object = value
|
|
198
|
+
return object.get_class()
|
|
199
|
+
return type_string(typeof(value))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
func _add_signal(lines: Array[String], mod: Dictionary) -> int:
|
|
203
|
+
var signal_name: String = str(mod.get("name", ""))
|
|
204
|
+
var signal_params: String = str(mod.get("params", ""))
|
|
205
|
+
|
|
206
|
+
var signal_line: String = "signal " + signal_name
|
|
207
|
+
if not signal_params.is_empty():
|
|
208
|
+
signal_line += "(" + signal_params + ")"
|
|
209
|
+
|
|
210
|
+
var insert_line: int = _signal_insertion_point(lines)
|
|
211
|
+
lines.insert(insert_line, signal_line)
|
|
212
|
+
return insert_line + 1
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
func _add_function(lines: Array[String], mod: Dictionary) -> int:
|
|
216
|
+
var func_name: String = str(mod.get("name", ""))
|
|
217
|
+
var func_params: String = str(mod.get("params", ""))
|
|
218
|
+
# A function that names no return type returns nothing, and says so, for the same reason
|
|
219
|
+
# the variable above does.
|
|
220
|
+
var return_type: String = str(mod.get("returnType", ""))
|
|
221
|
+
if return_type.is_empty():
|
|
222
|
+
return_type = "void"
|
|
223
|
+
var body: String = str(mod.get("body", "pass"))
|
|
224
|
+
var position: String = str(mod.get("position", "end"))
|
|
225
|
+
|
|
226
|
+
var func_lines: Array[String] = []
|
|
227
|
+
var func_decl: String = "func " + func_name + "(" + func_params + ") -> " + return_type + ":"
|
|
228
|
+
func_lines.append("")
|
|
229
|
+
func_lines.append(func_decl)
|
|
230
|
+
|
|
231
|
+
for bl: String in body.split("\n"):
|
|
232
|
+
func_lines.append("\t" + bl)
|
|
233
|
+
|
|
234
|
+
var insert_line: int = _function_insertion_point(lines, position)
|
|
235
|
+
|
|
236
|
+
for i: int in range(func_lines.size() - 1, -1, -1):
|
|
237
|
+
lines.insert(insert_line, func_lines[i])
|
|
238
|
+
|
|
239
|
+
return insert_line + 1
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
func _variable_insertion_point(lines: Array[String]) -> int:
|
|
243
|
+
var after_header: int = 0
|
|
244
|
+
var before_func: int = lines.size()
|
|
245
|
+
|
|
246
|
+
for i: int in range(lines.size()):
|
|
247
|
+
var line: String = lines[i].strip_edges()
|
|
248
|
+
if line.begins_with("extends ") or line.begins_with("class_name "):
|
|
249
|
+
after_header = i + 1
|
|
250
|
+
elif line.begins_with("signal "):
|
|
251
|
+
after_header = i + 1
|
|
252
|
+
elif line.begins_with("func ") or line.begins_with("static func "):
|
|
253
|
+
before_func = i
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
for i: int in range(after_header, before_func):
|
|
257
|
+
var line: String = lines[i].strip_edges()
|
|
258
|
+
if line.begins_with("var ") or line.begins_with("@export") or line.begins_with("@onready"):
|
|
259
|
+
after_header = i + 1
|
|
260
|
+
|
|
261
|
+
return after_header
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
func _signal_insertion_point(lines: Array[String]) -> int:
|
|
265
|
+
var after_header: int = 0
|
|
266
|
+
|
|
267
|
+
for i: int in range(lines.size()):
|
|
268
|
+
var line: String = lines[i].strip_edges()
|
|
269
|
+
if line.begins_with("extends ") or line.begins_with("class_name "):
|
|
270
|
+
after_header = i + 1
|
|
271
|
+
elif line.begins_with("signal "):
|
|
272
|
+
after_header = i + 1
|
|
273
|
+
elif (
|
|
274
|
+
line.begins_with("var ")
|
|
275
|
+
or line.begins_with("@export")
|
|
276
|
+
or line.begins_with("@onready")
|
|
277
|
+
or line.begins_with("func ")
|
|
278
|
+
):
|
|
279
|
+
break
|
|
280
|
+
|
|
281
|
+
return after_header
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
func _function_insertion_point(lines: Array[String], position: String) -> int:
|
|
285
|
+
match position:
|
|
286
|
+
"after_ready":
|
|
287
|
+
return _line_after_function(lines, "func _ready")
|
|
288
|
+
"after_init":
|
|
289
|
+
return _line_after_function(lines, "func _init")
|
|
290
|
+
_:
|
|
291
|
+
return lines.size()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# The line the next function starts on after the one whose declaration begins with `prefix`,
|
|
295
|
+
# or the end of the file when it is the last one or is not there at all.
|
|
296
|
+
func _line_after_function(lines: Array[String], prefix: String) -> int:
|
|
297
|
+
var inside: bool = false
|
|
298
|
+
for i: int in range(lines.size()):
|
|
299
|
+
var line: String = lines[i].strip_edges()
|
|
300
|
+
if line.begins_with(prefix):
|
|
301
|
+
inside = true
|
|
302
|
+
elif inside and (line.begins_with("func ") or line.begins_with("static func ")):
|
|
303
|
+
return i
|
|
304
|
+
return lines.size()
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
func _script_template(template_name: String) -> String:
|
|
308
|
+
match template_name:
|
|
309
|
+
"singleton":
|
|
310
|
+
return """## Singleton (Autoload) script
|
|
311
|
+
## Add to Project Settings -> Autoload to use as a global singleton
|
|
312
|
+
|
|
313
|
+
var _instance: Object = null
|
|
314
|
+
|
|
315
|
+
func _init() -> void:
|
|
316
|
+
\tif _instance != null:
|
|
317
|
+
\t\tpush_error("Singleton instance already exists!")
|
|
318
|
+
\t\treturn
|
|
319
|
+
\t_instance = self
|
|
320
|
+
|
|
321
|
+
func _ready() -> void:
|
|
322
|
+
\tpass
|
|
323
|
+
|
|
324
|
+
# Add your singleton methods here
|
|
325
|
+
"""
|
|
326
|
+
"state_machine":
|
|
327
|
+
return """## Finite State Machine implementation
|
|
328
|
+
|
|
329
|
+
signal state_changed(old_state: String, new_state: String)
|
|
330
|
+
|
|
331
|
+
var current_state: String = ""
|
|
332
|
+
## State objects by name. A state may define enter(), exit(), process(delta) and
|
|
333
|
+
## physics_process(delta); whichever it has are called.
|
|
334
|
+
var states: Dictionary = {}
|
|
335
|
+
|
|
336
|
+
func _ready() -> void:
|
|
337
|
+
\t_setup_states()
|
|
338
|
+
\tif states.size() > 0:
|
|
339
|
+
\t\tchange_state(str(states.keys()[0]))
|
|
340
|
+
|
|
341
|
+
func _setup_states() -> void:
|
|
342
|
+
\t# Override this to add states
|
|
343
|
+
\t# Example: states["idle"] = IdleState.new()
|
|
344
|
+
\tpass
|
|
345
|
+
|
|
346
|
+
func _process(delta: float) -> void:
|
|
347
|
+
\t_call_state(current_state, "process", [delta])
|
|
348
|
+
|
|
349
|
+
func _physics_process(delta: float) -> void:
|
|
350
|
+
\t_call_state(current_state, "physics_process", [delta])
|
|
351
|
+
|
|
352
|
+
func change_state(new_state: String) -> void:
|
|
353
|
+
\tif not states.has(new_state):
|
|
354
|
+
\t\tpush_error("State not found: " + new_state)
|
|
355
|
+
\t\treturn
|
|
356
|
+
|
|
357
|
+
\tvar old_state: String = current_state
|
|
358
|
+
\t_call_state(old_state, "exit", [])
|
|
359
|
+
\tcurrent_state = new_state
|
|
360
|
+
\t_call_state(current_state, "enter", [])
|
|
361
|
+
\tstate_changed.emit(old_state, new_state)
|
|
362
|
+
|
|
363
|
+
## Calls a method on the named state when it has one; a state without it is left alone.
|
|
364
|
+
func _call_state(state_name: String, method: String, args: Array) -> void:
|
|
365
|
+
\tif state_name.is_empty() or not states.has(state_name):
|
|
366
|
+
\t\treturn
|
|
367
|
+
\tvar state: Object = states[state_name]
|
|
368
|
+
\tif state != null and state.has_method(method):
|
|
369
|
+
\t\tstate.callv(method, args)
|
|
370
|
+
"""
|
|
371
|
+
"component":
|
|
372
|
+
return """## Component pattern - attach to nodes to add behavior
|
|
373
|
+
|
|
374
|
+
@export var enabled: bool = true
|
|
375
|
+
|
|
376
|
+
func _ready() -> void:
|
|
377
|
+
\tif not enabled:
|
|
378
|
+
\t\tset_process(false)
|
|
379
|
+
\t\tset_physics_process(false)
|
|
380
|
+
|
|
381
|
+
func _process(_delta: float) -> void:
|
|
382
|
+
\tif not enabled:
|
|
383
|
+
\t\treturn
|
|
384
|
+
\t# Component logic here
|
|
385
|
+
\tpass
|
|
386
|
+
|
|
387
|
+
func enable() -> void:
|
|
388
|
+
\tenabled = true
|
|
389
|
+
\tset_process(true)
|
|
390
|
+
\tset_physics_process(true)
|
|
391
|
+
|
|
392
|
+
func disable() -> void:
|
|
393
|
+
\tenabled = false
|
|
394
|
+
\tset_process(false)
|
|
395
|
+
\tset_physics_process(false)
|
|
396
|
+
"""
|
|
397
|
+
"resource":
|
|
398
|
+
return """## Custom Resource - save and load data
|
|
399
|
+
|
|
400
|
+
@export var id: String = ""
|
|
401
|
+
@export var display_name: String = ""
|
|
402
|
+
@export var description: String = ""
|
|
403
|
+
|
|
404
|
+
func _init(p_id: String = "", p_name: String = "", p_desc: String = "") -> void:
|
|
405
|
+
\tid = p_id
|
|
406
|
+
\tdisplay_name = p_name
|
|
407
|
+
\tdescription = p_desc
|
|
408
|
+
|
|
409
|
+
func duplicate_resource() -> Resource:
|
|
410
|
+
\tvar new_resource: Resource = duplicate()
|
|
411
|
+
\treturn new_resource
|
|
412
|
+
"""
|
|
413
|
+
_:
|
|
414
|
+
return """func _ready() -> void:
|
|
415
|
+
\tpass
|
|
416
|
+
|
|
417
|
+
func _process(_delta: float) -> void:
|
|
418
|
+
\tpass
|
|
419
|
+
"""
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env -S godot --headless --script
|
|
2
|
+
extends SceneTree
|
|
3
|
+
|
|
4
|
+
# The command line the server runs is `godot --headless --path <project> --script <this>
|
|
5
|
+
# <operation> @file:<params.json>`, and one JSON object on stdout is the answer. Everything an
|
|
6
|
+
# operation does lives in a sibling module; this file is the command table and the wire format.
|
|
7
|
+
#
|
|
8
|
+
# Each module is preloaded by a path relative to this script rather than a res:// one, because
|
|
9
|
+
# the operations directory ships inside the server package and is handed to the engine as an
|
|
10
|
+
# absolute path outside the project. A res:// path would only resolve in the test fixture.
|
|
11
|
+
|
|
12
|
+
const AudioBuses = preload("audio_buses.gd")
|
|
13
|
+
const ClassCache = preload("class_cache.gd")
|
|
14
|
+
const ClassDbQueries = preload("classdb_queries.gd")
|
|
15
|
+
const Dependencies = preload("dependencies.gd")
|
|
16
|
+
const GdscriptAnalysis = preload("gdscript_analysis.gd")
|
|
17
|
+
const GdscriptAuthoring = preload("gdscript_authoring.gd")
|
|
18
|
+
const ImportPipeline = preload("import_pipeline.gd")
|
|
19
|
+
const InputActions = preload("input_actions.gd")
|
|
20
|
+
const Log = preload("logger.gd")
|
|
21
|
+
const Plugins = preload("plugins.gd")
|
|
22
|
+
const ProjectConfig = preload("project_config.gd")
|
|
23
|
+
const ProjectDiagnostics = preload("project_diagnostics.gd")
|
|
24
|
+
const ResourceFiles = preload("resource_files.gd")
|
|
25
|
+
|
|
26
|
+
var _log: Log
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
func _init() -> void:
|
|
30
|
+
var args: PackedStringArray = OS.get_cmdline_args()
|
|
31
|
+
_log = Log.new("--debug-godot" in args)
|
|
32
|
+
|
|
33
|
+
# The script path is the argument after --script, so the operation and its parameters are
|
|
34
|
+
# the two that follow it.
|
|
35
|
+
var script_index: int = args.find("--script")
|
|
36
|
+
if script_index == -1:
|
|
37
|
+
_log.error("Could not find --script argument")
|
|
38
|
+
quit(1)
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
var params_index: int = script_index + 3
|
|
42
|
+
if args.size() <= params_index:
|
|
43
|
+
_log.error("Usage: godot --headless --script godot_operations.gd <operation> <json_params>")
|
|
44
|
+
_log.error("Not enough command-line arguments provided.")
|
|
45
|
+
quit(1)
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
_log.debug("All arguments: " + str(args))
|
|
49
|
+
|
|
50
|
+
var operation: String = args[script_index + 2]
|
|
51
|
+
var params: Variant = _read_params(args[params_index])
|
|
52
|
+
if not params is Dictionary:
|
|
53
|
+
quit(1)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
_log.info("Executing operation: " + operation)
|
|
57
|
+
|
|
58
|
+
var payload: Dictionary = _run(operation, params)
|
|
59
|
+
if payload.is_empty():
|
|
60
|
+
quit(1)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
print(JSON.stringify(payload))
|
|
64
|
+
quit()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# The operation parameters. They arrive as a path to a JSON file rather than as JSON on argv
|
|
68
|
+
# because a blob on the command line runs into Windows parsing of \t, \r and \" whatever the
|
|
69
|
+
# quoting. Null means the parameters could not be read, and the reason is already on stderr.
|
|
70
|
+
func _read_params(argument: String) -> Variant:
|
|
71
|
+
var params_json: String = argument
|
|
72
|
+
|
|
73
|
+
if params_json.begins_with("@file:"):
|
|
74
|
+
var params_file_path: String = params_json.substr(6)
|
|
75
|
+
var params_file: FileAccess = FileAccess.open(params_file_path, FileAccess.READ)
|
|
76
|
+
if params_file == null:
|
|
77
|
+
_log.error("Failed to open params file: " + params_file_path)
|
|
78
|
+
return null
|
|
79
|
+
params_json = params_file.get_as_text()
|
|
80
|
+
params_file.close()
|
|
81
|
+
|
|
82
|
+
_log.debug("Params JSON: " + params_json)
|
|
83
|
+
|
|
84
|
+
var json: JSON = JSON.new()
|
|
85
|
+
if json.parse(params_json) != OK:
|
|
86
|
+
_log.error("Failed to parse JSON parameters: " + params_json)
|
|
87
|
+
_log.error("JSON Error: " + json.get_error_message() + " at line " + str(json.get_error_line()))
|
|
88
|
+
return null
|
|
89
|
+
|
|
90
|
+
var params: Variant = json.get_data()
|
|
91
|
+
if not params is Dictionary:
|
|
92
|
+
_log.error("Parameters must be a JSON object: " + params_json)
|
|
93
|
+
return null
|
|
94
|
+
return params
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# The payload one operation answers with, or an empty dictionary when it failed or nobody
|
|
98
|
+
# owns the name.
|
|
99
|
+
func _run(operation: String, params: Dictionary) -> Dictionary:
|
|
100
|
+
var payload: Dictionary = {}
|
|
101
|
+
|
|
102
|
+
match operation:
|
|
103
|
+
# Resource files
|
|
104
|
+
"get_uid":
|
|
105
|
+
payload = ResourceFiles.new(_log).get_uid(params)
|
|
106
|
+
"resave_resources":
|
|
107
|
+
payload = ResourceFiles.new(_log).resave_resources(params)
|
|
108
|
+
"refresh_class_cache":
|
|
109
|
+
payload = ClassCache.new(_log).refresh_class_cache(params)
|
|
110
|
+
|
|
111
|
+
# Import and export pipeline
|
|
112
|
+
"get_import_status":
|
|
113
|
+
payload = ImportPipeline.new(_log).get_import_status(params)
|
|
114
|
+
"get_import_options":
|
|
115
|
+
payload = ImportPipeline.new(_log).get_import_options(params)
|
|
116
|
+
"set_import_options":
|
|
117
|
+
payload = ImportPipeline.new(_log).set_import_options(params)
|
|
118
|
+
"reimport_resource":
|
|
119
|
+
payload = ImportPipeline.new(_log).reimport_resource(params)
|
|
120
|
+
"list_export_presets":
|
|
121
|
+
payload = ImportPipeline.new(_log).list_export_presets(params)
|
|
122
|
+
"validate_project":
|
|
123
|
+
payload = ImportPipeline.new(_log).validate_project(params)
|
|
124
|
+
|
|
125
|
+
# Dependencies and project inspection
|
|
126
|
+
"get_dependencies":
|
|
127
|
+
payload = Dependencies.new(_log).get_dependencies(params)
|
|
128
|
+
"find_resource_usages":
|
|
129
|
+
payload = Dependencies.new(_log).find_resource_usages(params)
|
|
130
|
+
"get_project_health":
|
|
131
|
+
payload = ProjectDiagnostics.new(_log).get_project_health(params)
|
|
132
|
+
|
|
133
|
+
# project.godot
|
|
134
|
+
"get_project_setting":
|
|
135
|
+
payload = ProjectConfig.new(_log).get_project_setting(params)
|
|
136
|
+
"set_project_setting":
|
|
137
|
+
payload = ProjectConfig.new(_log).set_project_setting(params)
|
|
138
|
+
"add_autoload":
|
|
139
|
+
payload = ProjectConfig.new(_log).add_autoload(params)
|
|
140
|
+
"remove_autoload":
|
|
141
|
+
payload = ProjectConfig.new(_log).remove_autoload(params)
|
|
142
|
+
"list_autoloads":
|
|
143
|
+
payload = ProjectConfig.new(_log).list_autoloads(params)
|
|
144
|
+
"set_main_scene":
|
|
145
|
+
payload = ProjectConfig.new(_log).set_main_scene(params)
|
|
146
|
+
|
|
147
|
+
# GDScript files
|
|
148
|
+
"create_script":
|
|
149
|
+
payload = GdscriptAuthoring.new(_log).create_gdscript(params)
|
|
150
|
+
"modify_script":
|
|
151
|
+
payload = GdscriptAuthoring.new(_log).modify_gdscript(params)
|
|
152
|
+
"get_script_info":
|
|
153
|
+
payload = GdscriptAnalysis.new(_log).get_gdscript_info(params)
|
|
154
|
+
|
|
155
|
+
# Plugins and input
|
|
156
|
+
"list_plugins":
|
|
157
|
+
payload = Plugins.new(_log).list_plugins(params)
|
|
158
|
+
"enable_plugin":
|
|
159
|
+
payload = Plugins.new(_log).enable_plugin(params)
|
|
160
|
+
"disable_plugin":
|
|
161
|
+
payload = Plugins.new(_log).disable_plugin(params)
|
|
162
|
+
"add_input_action":
|
|
163
|
+
payload = InputActions.new(_log).add_input_action(params)
|
|
164
|
+
|
|
165
|
+
# Audio
|
|
166
|
+
"create_audio_bus":
|
|
167
|
+
payload = AudioBuses.new(_log).create_audio_bus(params)
|
|
168
|
+
"get_audio_buses":
|
|
169
|
+
payload = AudioBuses.new(_log).get_audio_buses(params)
|
|
170
|
+
"set_audio_bus_effect":
|
|
171
|
+
payload = AudioBuses.new(_log).set_audio_bus_effect(params)
|
|
172
|
+
"set_audio_bus_volume":
|
|
173
|
+
payload = AudioBuses.new(_log).set_audio_bus_volume(params)
|
|
174
|
+
|
|
175
|
+
# ClassDB
|
|
176
|
+
"query_classes":
|
|
177
|
+
payload = ClassDbQueries.new(_log).query_classes(params)
|
|
178
|
+
"query_class_info":
|
|
179
|
+
payload = ClassDbQueries.new(_log).query_class_info(params)
|
|
180
|
+
"inspect_inheritance":
|
|
181
|
+
payload = ClassDbQueries.new(_log).inspect_inheritance(params)
|
|
182
|
+
|
|
183
|
+
_:
|
|
184
|
+
_log.error("Unknown operation: " + operation)
|
|
185
|
+
|
|
186
|
+
return payload
|