Initial Void Bastion 0.20 prototype

This commit is contained in:
Emporex committed 2026-10-03 21:28:56 +02:00
commit 7362ca0f02
251 files changed
+29688

No files matched your search

+171
View File
@@ -0,0 +1,171 @@
@tool
extends RefCounted
# script.create — write a new .gd file with an extends/class_name header.
# Params: {path: "res://scripts/Player.gd", extends?: "Node", class_name?: "Player",
# attach_to_node?: "Player", overwrite?: false}
static func create(params: Dictionary) -> Dictionary:
var path: String = params.get("path", "")
var base_class: String = params.get("extends", "Node")
var class_name_val: String = params.get("class_name", "")
var overwrite: bool = params.get("overwrite", false)
var attach_to: String = params.get("attach_to_node", "")
if path == "" or not path.begins_with("res://") or not path.ends_with(".gd"):
return _err(-32602, "'path' must be 'res://...' ending in .gd")
if FileAccess.file_exists(path) and not overwrite:
return _err(-32602, "file exists (pass overwrite:true to replace): %s" % path)
var dir_path := path.get_base_dir()
if not DirAccess.dir_exists_absolute(dir_path):
var derr := DirAccess.make_dir_recursive_absolute(dir_path)
if derr != OK:
return _err(-32001, "mkdir failed (%d): %s" % [derr, dir_path])
var body := "extends %s\n" % base_class
if class_name_val != "":
body += "class_name %s\n" % class_name_val
body += "\n"
var f := FileAccess.open(path, FileAccess.WRITE)
if f == null:
return _err(-32001, "failed to open for write: %s" % path)
f.store_string(body)
f.close()
# Let the editor discover the new file so load() succeeds immediately.
EditorInterface.get_resource_filesystem().update_file(path)
var data := {"path": path, "extends": base_class, "class_name": class_name_val}
if attach_to != "":
var attach_res := _attach_script_to_node(attach_to, path)
if attach_res.has("error"):
return attach_res
data["attached_to"] = attach_to
return _ok(data)
# script.attach — attach an existing script to a node in the currently-edited scene.
# Params: {node_path, script_path}
static func attach(params: Dictionary) -> Dictionary:
var node_path: String = params.get("node_path", "")
var script_path: String = params.get("script_path", "")
if node_path == "" or script_path == "":
return _err(-32602, "missing 'node_path' or 'script_path'")
return _attach_script_to_node(node_path, script_path)
# script.patch — targeted edits to an existing .gd file. Two modes:
# replacements: [{old, new}] — each 'old' must appear exactly once in the file;
# fails atomically if any replacement is ambiguous
# or missing. Useful for surgical edits.
# full_source: string — overwrite the whole file (same as script.create
# overwrite, kept here for symmetry).
# After writing, the tool parse-checks the result via ResourceLoader.load(); if
# that fails, the original file is restored and an error is returned.
# Params: {path, replacements?: [{old, new}], full_source?: string, dry_run?: false}
static func patch(params: Dictionary) -> Dictionary:
var path: String = params.get("path", "")
var replacements: Array = params.get("replacements", [])
var full_source: String = params.get("full_source", "")
var dry_run: bool = params.get("dry_run", false)
if path == "" or not path.ends_with(".gd"):
return _err(-32602, "'path' must be a .gd file")
if not FileAccess.file_exists(path):
return _err(-32001, "file not found: %s" % path)
if replacements.is_empty() and full_source == "":
return _err(-32602, "provide either 'replacements' or 'full_source'")
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
return _err(-32001, "could not read %s" % path)
var original := f.get_as_text()
f.close()
var patched: String = original
var applied: Array = []
if full_source != "":
patched = full_source
applied.append("full_source")
else:
for entry in replacements:
if typeof(entry) != TYPE_DICTIONARY or not entry.has("old") or not entry.has("new"):
return _err(-32602, "each replacement needs {old, new}")
var needle: String = str(entry.old)
var replacement: String = str(entry.new)
var count := patched.count(needle)
if count == 0:
return _err(-32602, "'old' string not found (would leave file unchanged): %s" % needle.substr(0, 60))
if count > 1:
return _err(-32602, "'old' string appears %d times — ambiguous. Include more context to make it unique: %s" % [count, needle.substr(0, 60)])
patched = patched.replace(needle, replacement)
applied.append("replaced %d chars" % needle.length())
if patched == original:
return _ok({"path": path, "changed": false, "note": "patched content identical to original"})
if dry_run:
return _ok({
"path": path,
"changed": true,
"applied": applied,
"new_length": patched.length(),
"original_length": original.length(),
})
# Write, then parse-check. If parse fails, restore.
var wf := FileAccess.open(path, FileAccess.WRITE)
if wf == null:
return _err(-32001, "could not write %s" % path)
wf.store_string(patched)
wf.close()
EditorInterface.get_resource_filesystem().update_file(path)
# Force a fresh parse — ResourceLoader.load returns a cached/placeholder
# Script for malformed sources, so we can't just null-check. Call reload()
# which actually re-parses and returns an Error.
var check := ResourceLoader.load(path, "Script", ResourceLoader.CACHE_MODE_REPLACE)
var parse_ok := false
if check != null and check is GDScript:
parse_ok = (check.reload(true) == OK)
if not parse_ok:
# Restore original to keep the file loadable.
var rf := FileAccess.open(path, FileAccess.WRITE)
if rf:
rf.store_string(original)
rf.close()
EditorInterface.get_resource_filesystem().update_file(path)
return _err(-32001, "patched script failed to parse — changes rolled back. Check Godot's Output panel for the exact error.")
return _ok({
"path": path,
"changed": true,
"applied": applied,
"new_length": patched.length(),
})
static func _attach_script_to_node(node_path: String, script_path: String) -> Dictionary:
var root := EditorInterface.get_edited_scene_root()
if root == null:
return _err(-32001, "no scene open")
var node: Node = root if node_path == "." else root.get_node_or_null(node_path)
if node == null:
return _err(-32001, "node not found: %s" % node_path)
var script := ResourceLoader.load(script_path, "Script") as Script
if script == null:
return _err(-32001, "failed to load script: %s" % script_path)
node.set_script(script)
EditorInterface.mark_scene_as_unsaved()
return _ok({"node_path": node_path, "script": script_path})
static func _ok(data) -> Dictionary:
return {"data": data}
static func _err(code: int, msg: String) -> Dictionary:
return {"error": {"code": code, "message": msg}}