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

+187
View File
@@ -0,0 +1,187 @@
# Auto-run driver for run.scene_headless (input simulation, multi-screenshot,
# state dump, deterministic seed). Reads a per-call config from
# res://.godot/agent_tools/headless/config.json, instances the target scene,
# injects scripted input per frame, captures screenshots at specified frames,
# optionally writes a final-state JSON dump, then quits.
@tool
extends Node
const CONFIG_PATH := "res://.godot/agent_tools/headless/config.json"
const Coerce := preload("res://addons/agent_tools/tools/_coerce.gd")
var _input_script: Array = []
var _screenshots: Array = [] # [{frame, path}, ...]
var _state_dump: bool = false
var _state_dump_path: String = ""
var _quit_frame: int = 120
var _frame: int = 0
var _target_inst: Node = null
func _ready() -> void:
var f := FileAccess.open(CONFIG_PATH, FileAccess.READ)
if f == null:
push_error("[headless_driver] config not found at %s" % CONFIG_PATH)
get_tree().quit(2)
return
var parsed = JSON.parse_string(f.get_as_text())
f.close()
if typeof(parsed) != TYPE_DICTIONARY:
push_error("[headless_driver] malformed config")
get_tree().quit(2)
return
# Seed the global RNG before instancing so the target scene's randomness
# (randi/randf in _ready, etc.) is deterministic per-seed.
if parsed.has("seed"):
seed(int(parsed.seed))
_input_script = parsed.get("input_script", [])
_screenshots = parsed.get("screenshots", [])
_state_dump = bool(parsed.get("state_dump", false))
_state_dump_path = str(parsed.get("state_dump_path", ""))
_quit_frame = int(parsed.get("quit_frame", 120))
var target_scene_path: String = parsed.get("target_scene", "")
if target_scene_path == "":
push_error("[headless_driver] config missing 'target_scene'")
get_tree().quit(2)
return
var packed = load(target_scene_path)
if packed == null:
push_error("[headless_driver] failed to load target %s" % target_scene_path)
get_tree().quit(2)
return
_target_inst = packed.instantiate()
add_child(_target_inst)
print("[headless_driver] driving %s for %d frames, %d events, %d screenshots%s" %
[target_scene_path, _quit_frame, _input_script.size(), _screenshots.size(),
", state_dump=on" if _state_dump else ""])
func _process(_delta: float) -> void:
# 1. Inject input events scheduled for this frame.
for spec in _input_script:
if int(spec.get("frame", -1)) == _frame:
_inject(spec)
# 2. Capture screenshots scheduled for this frame.
for shot in _screenshots:
if int(shot.get("frame", -1)) == _frame:
_capture(str(shot.get("path", "")))
if _frame >= _quit_frame:
if _state_dump and _state_dump_path != "":
_write_state_dump()
print("[headless_driver] done at frame %d" % _frame)
get_tree().quit(0)
return
_frame += 1
func _inject(spec: Dictionary) -> void:
match spec.get("type", ""):
"action_press":
Input.action_press(spec.get("action", ""), float(spec.get("strength", 1.0)))
"action_release":
Input.action_release(spec.get("action", ""))
"action_tap":
Input.action_press(spec.get("action", ""), float(spec.get("strength", 1.0)))
Input.action_release(spec.get("action", ""))
"key":
var ek := InputEventKey.new()
var kc = spec.get("keycode", 0)
if kc is String:
ek.physical_keycode = OS.find_keycode_from_string(kc)
else:
ek.physical_keycode = int(kc)
ek.pressed = bool(spec.get("pressed", true))
Input.parse_input_event(ek)
"mouse_click":
var pos = spec.get("position", [0, 0])
var v := Vector2(pos[0], pos[1])
var button := int(spec.get("button", 1))
var down := InputEventMouseButton.new()
down.button_index = button
down.pressed = true
down.position = v
down.global_position = v
Input.parse_input_event(down)
var up := InputEventMouseButton.new()
up.button_index = button
up.pressed = false
up.position = v
up.global_position = v
Input.parse_input_event(up)
"mouse_motion":
var pos = spec.get("position", [0, 0])
var em := InputEventMouseMotion.new()
em.position = Vector2(pos[0], pos[1])
em.global_position = em.position
Input.parse_input_event(em)
_:
push_warning("[headless_driver] unknown event type: %s" % spec.get("type", "<missing>"))
func _capture(path: String) -> void:
if path == "":
return
var vp := get_tree().root.get_viewport()
if vp == null:
push_warning("[headless_driver] no root viewport")
return
var tex := vp.get_texture()
if tex == null:
push_warning("[headless_driver] viewport has no texture (running under dummy renderer?)")
return
var img := tex.get_image()
if img == null or img.is_empty():
push_warning("[headless_driver] failed to capture image at frame %d" % _frame)
return
var dir := path.get_base_dir()
if not DirAccess.dir_exists_absolute(dir):
DirAccess.make_dir_recursive_absolute(dir)
var err := img.save_png(path)
if err != OK:
push_warning("[headless_driver] save_png failed: %d" % err)
else:
print("[headless_driver] frame %d -> %s" % [_frame, path])
func _write_state_dump() -> void:
if _target_inst == null:
return
var snapshot := _node_snapshot(_target_inst, _target_inst)
var f := FileAccess.open(_state_dump_path, FileAccess.WRITE)
if f == null:
push_warning("[headless_driver] failed to open state dump path: %s" % _state_dump_path)
return
f.store_string(JSON.stringify(snapshot, " "))
f.close()
print("[headless_driver] state dump -> %s" % _state_dump_path)
# Mirrors scene.inspect's shape, plus a small selection of commonly-useful
# properties. Kept narrow on purpose — full get_property_list() dumps balloon.
static func _node_snapshot(node: Node, scene_root: Node) -> Dictionary:
var d := {
"name": String(node.name),
"class": node.get_class(),
"node_path": "." if node == scene_root else String(scene_root.get_path_to(node)),
"script": "",
"properties": {},
"children": [],
}
var scr := node.get_script() as Script
if scr:
d.script = scr.resource_path
# Common props worth verifying, if present on this node type.
for prop_name in ["visible", "position", "global_position", "size", "text",
"value", "modulate", "color", "rotation", "scale", "disabled", "pressed"]:
if prop_name in node:
d.properties[prop_name] = Coerce.to_json(node.get(prop_name))
for child in node.get_children():
if child.owner == scene_root or child == scene_root:
d.children.append(_node_snapshot(child, scene_root))
return d
@@ -0,0 +1 @@
uid://iko41fhoo3mm
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://addons/agent_tools/headless/driver.gd" id="1"]
[node name="HeadlessDriver" type="Node"]
script = ExtResource("1")