The script template
Copy this. It is the exact file used to verify the behaviour on this page.@tool on a -s script is a no-op — Engine.is_editor_hint() is false here.
Exit codes lie
Gate on stderr, not on$?. Streams are cleanly separated: print() goes to stdout;
printerr(), push_error(), push_warning() and all engine ERROR: / WARNING: lines
go to stderr. Grep for SCRIPT ERROR, Parse Error, Can't load script, and ERROR:.
Check syntax before you run anything. --check-only parses a script without executing
it, in well under a second — the cheapest possible gate, and it catches the parse errors that
would otherwise exit 0 and look like success:
WARNING: ObjectDB instances leaked at exit prints on every run,
including fully successful ones. Do not gate on it.
Finding the binary
On macOS the engine is at/Applications/Summer.app/Contents/MacOS/Summer. From inside a
running script, ask the engine directly rather than guessing:
.app wrapper, so it is safe to re-invoke
directly for self-spawning work.
What works
Verified by running each against the shipped binary.Collision shapes from a mesh
Collision shapes from a mesh
create_trimesh_shape() and create_convex_shape() both work and round-trip through
.tres losslessly. A BoxMesh produces 36 face vec3s (12 triangles) trimesh and an
8-point convex hull.Simplification is effective: a 32x16 sphere gives a 514-point convex hull, or 32 points
with create_convex_shape(true, true).LOD generation
LOD generation
ImporterMesh is instantiable outside the editor. In Summer the signature takes three
arguments:get_mesh() returns an ArrayMesh that saves and reloads
cleanly. Note generate_shadow_mesh is not exposed.TileSet, Theme, Shader, AudioBusLayout
TileSet, Theme, Shader, AudioBusLayout
All four author and round-trip through
.tres.TileSet keeps atlas sources, physics layers, terrain sets, navigation layers, custom data
layers and per-tile collision polygons. One gotcha: add_physics_layer(),
add_terrain_set(), add_navigation_layer() and add_custom_data_layer() return void,
not the new index, so var i := ts.add_physics_layer() is a parse error.Shader source survives byte-identical and uniforms introspect correctly — but this is
parsing, not GPU compilation. The dummy renderer never compiles the shader, so headless
will not catch driver-level compile errors.AudioBusLayout works with the Dummy audio driver; bus volumes, mutes and effects all
persist.Physics and raycasting
Physics and raycasting
This is the real story: headless can simulate, it just cannot see. Physics is entirely
CPU-side and completely unaffected by the dummy renderer.3D and 2D
intersect_ray, intersect_point and intersect_shape all work, returning full
hit dictionaries with position, normal, collider and RID. Rigid bodies actually fall under
gravity.Prerequisite: await physics_frame after add_child(), or direct_space_state returns
nothing.Importing assets
The best fix is to need no import at all. Two routes avoid this entirely, and both are described under shipping assets below:ResourceSaver.save()to.tresor.res— no import pass, no editor, no.godot/.- Write a
.ddsor a.po— the formats that load raw and ship as-is.
.import
sidecars — run the import pass:
.import file next to your asset and the compiled .ctex into
.godot/imported/. Afterwards load() returns a CompressedTexture2D and everything
behaves normally.
-s script runs never create .godot/ at all. Only --import (or a normal editor or
game run) builds the import database. If you have only ever run scripts, the directory does
not exist yet.--import costs roughly 5 seconds even with nothing to do, about 15x a trivial script run.
It is a full editor boot — it loads the editor layout and starts the local API server,
with the binding consequences described above. Do not run it speculatively.Do not ship a game that depends on raw source files
There is a tempting shortcut:Image.load_from_file("res://raw.png") reads the file
directly and needs no import pass at all. Equivalents exist for audio
(AudioStreamOggVorbis.load_from_file), fonts (FontFile.load_dynamic_font) and models
(GLTFDocument.append_from_file).
Three routes that survive export:
- Author a real resource and save it.
ResourceSaver.save()to.tresor.resneeds no import pass, no editor, and no.godot/at all, and the result ships correctly. Verified forImageTexture,Image,AudioStreamWAV,AudioStreamOggVorbis,AudioStreamMP3,FontFile(which embeds the whole TTF),ArrayMesh,TranslationandAudioBusLayout. One trap:PortableCompressedTexture2Dsaves with error 0 and reloads empty — useImageTexture. - Import properly, then use ordinary
load(). The compiled.ctexalways ships. - Write a
.ddsdirectly. It is the one image format that needs no import pass, loads raw, decodes to real pixels, and ships as-is inside the pack — a 128-byte header plus pixel data, emittable from a few lines of script. Load it withResourceLoader.load(), which returns anImageTexture; noteImage.load_from_file()does not read DDS and returns null for it.
.godot/imported/ is load-bearing, not a disposable cache. Delete it and every imported
asset returns null, even with all the .import files still present. Ignore any advice that
tells you to clean it.Exporting a build
Export works headless from a hand-writtenexport_presets.cfg. Summer bundles macOS export
templates inside the app, so no download is needed for a Mac build:
OS.has_feature("template") == true and Engine.is_editor_hint() == false.
--export-pack produces a .pck instead.
Web export is not available on this build at all. Summer ships as a Mono (C#/.NET) build, and
Godot 4 does not support exporting to Web from a Mono build. This is an architectural
property of the binary, not a missing template.
export_filter="all_resources". The dependency-based resources filter exits non-zero
and produces an empty 112-byte pack for a project whose main scene has no resource
dependencies. Be aware all_resources packs everything, including every stray .gd file in
the project.
Two errors print at the start of every export and are harmless:
Parent node is busy setting up children, add_child() failed and
Condition "!is_inside_tree()" is true from http_request.cpp.
What does not work
Anything needing editor context has a supported route. Adding
--editor gives you a live
EditorNode and the full EditorInterface — 70 bound methods including save_scene,
open_scene_from_path and get_resource_filesystem() — even under --headless, and even
with no plugin installed:
EditorInterface unlocks.
Draw calls and the video adapter name are useful liveness probes. If
Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME) reads 0.0 or
RenderingServer.get_video_adapter_name() is empty, you are not rendering and never will be.
Performance
Startup overhead is roughly 0.3 to 0.4 seconds, low enough to call the binary in a tight loop.For AI agents
If you are an agent automating Summer, the things that will silently burn you — note that every one of them fails by appearing to succeed:--headlessrenders nothing.get_texture()looks fine;get_image()returns null. Never plan around a headless screenshot.- Use
_init(), never_run()._run()hangs with zero output. - Exit code 0 means nothing, and a runtime error means no exit code at all. Parse errors and missing script files exit 0, so grep stderr. Runtime errors hang the process forever, so also impose an external wall-clock timeout — stderr cannot help you if the process never returns.
await process_frameafteradd_child(), or in-tree and physics APIs fail before the node is really in the tree.- Write a file, then
--importit, orload()will not find it. - Never read a raw source file at runtime. Exporting strips them.
Image.load_from_fileand friends return null in the shipped game, andFontFile.load_dynamic_fontreturns error 0 with zero faces. - Verify a pack from an empty directory.
--main-packis silently ignored when the working directory holds aproject.godot, so checking your export from the project folder reports a false pass.
OS.get_executable_path() rather than assuming a path, and never
assume it is called godot.
