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.
Files changed (37) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +125 -0
  3. package/build/cli.js +30353 -0
  4. package/build/godot/addons/auto_reload/auto_reload.gd +89 -0
  5. package/build/godot/addons/auto_reload/plugin.cfg +6 -0
  6. package/build/godot/addons/gdharness_editor/bridge_client.gd +197 -0
  7. package/build/godot/addons/gdharness_editor/plugin.cfg +6 -0
  8. package/build/godot/addons/gdharness_editor/plugin.gd +88 -0
  9. package/build/godot/addons/gdharness_editor/tool_executor.gd +104 -0
  10. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +397 -0
  11. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +85 -0
  12. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +427 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +885 -0
  14. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +292 -0
  15. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +77 -0
  16. package/build/godot/addons/gdharness_runtime/runtime_input.gd +254 -0
  17. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +283 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_values.gd +169 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +119 -0
  20. package/build/godot/operations/audio_buses.gd +125 -0
  21. package/build/godot/operations/class_cache.gd +180 -0
  22. package/build/godot/operations/classdb_queries.gd +252 -0
  23. package/build/godot/operations/dependencies.gd +293 -0
  24. package/build/godot/operations/file_walk.gd +55 -0
  25. package/build/godot/operations/gdscript_analysis.gd +288 -0
  26. package/build/godot/operations/gdscript_authoring.gd +419 -0
  27. package/build/godot/operations/godot_operations.gd +186 -0
  28. package/build/godot/operations/import_pipeline.gd +390 -0
  29. package/build/godot/operations/input_actions.gd +237 -0
  30. package/build/godot/operations/logger.gd +33 -0
  31. package/build/godot/operations/plugins.gd +159 -0
  32. package/build/godot/operations/project_config.gd +153 -0
  33. package/build/godot/operations/project_diagnostics.gd +159 -0
  34. package/build/godot/operations/resource_files.gd +132 -0
  35. package/build/godot/operations/serialisation.gd +154 -0
  36. package/build/index.js +29252 -0
  37. 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