Initial Void Bastion 0.20 prototype
This commit is contained in:
commit
7362ca0f02
251 files changed
+29688
No files matched your search
@@ -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}}
|
||||
Reference in new issue
Block a user