summer-engine 2.7.0 → 2.8.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.
- package/.claude-plugin/plugin.json +2 -1
- package/.codex-plugin/plugin.json +3 -2
- package/.cursor-plugin/plugin.json +2 -1
- package/.opencode/INSTALL.md +1 -1
- package/.opencode/plugins/summer.js +1 -1
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +28 -0
- package/GEMINI.md +5 -4
- package/README.md +58 -21
- package/assets/autopilot/README.md +51 -0
- package/assets/autopilot/autopilot.gd +222 -0
- package/assets/autopilot/probe_base.gd +262 -0
- package/assets/autopilot/run.sh +78 -0
- package/commands/gameskill.md +11 -2
- package/dist/bin/summer.js +8 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +53 -0
- package/dist/commands/create.d.ts +1 -0
- package/dist/commands/create.js +38 -6
- package/dist/commands/list.js +1 -1
- package/dist/commands/login.d.ts +19 -0
- package/dist/commands/login.js +136 -37
- package/dist/commands/logout.js +3 -16
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +19 -0
- package/dist/commands/mcp.js +7 -2
- package/dist/commands/open.js +1 -1
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +20 -0
- package/dist/commands/releases.d.ts +2 -0
- package/dist/commands/releases.js +20 -0
- package/dist/commands/run.js +1 -1
- package/dist/lib/api-client.d.ts +27 -8
- package/dist/lib/api-client.js +65 -14
- package/dist/lib/auth.d.ts +40 -12
- package/dist/lib/auth.js +173 -37
- package/dist/lib/cloud/engine-bridge.d.ts +17 -4
- package/dist/lib/cloud/engine-bridge.js +32 -6
- package/dist/lib/config.d.ts +20 -0
- package/dist/lib/config.js +142 -0
- package/dist/lib/creator.d.ts +93 -0
- package/dist/lib/creator.js +393 -0
- package/dist/lib/engine.d.ts +35 -2
- package/dist/lib/engine.js +240 -6
- package/dist/lib/public-surface-paths.d.ts +13 -0
- package/dist/lib/public-surface-paths.js +42 -0
- package/dist/lib/skills-registry.d.ts +8 -0
- package/dist/lib/skills-registry.js +9 -0
- package/dist/lib/store.d.ts +14 -0
- package/dist/lib/store.js +145 -0
- package/dist/lib/summer-compatibility.d.ts +6 -0
- package/dist/lib/summer-compatibility.js +22 -0
- package/dist/mcp/server.d.ts +8 -1
- package/dist/mcp/server.js +25 -4
- package/dist/mcp/tools/creator-tools.d.ts +2 -0
- package/dist/mcp/tools/creator-tools.js +105 -0
- package/dist/mcp/tools/debug-tools.d.ts +12 -0
- package/dist/mcp/tools/debug-tools.js +100 -1
- package/dist/mcp/tools/generate-tools.js +16 -6
- package/dist/mcp/tools/project-tools.js +131 -16
- package/dist/mcp/tools/scene-tools.js +166 -66
- package/dist/mcp/tools/visual-tools.js +32 -8
- package/dist/mcp/tools/with-engine.js +51 -19
- package/hooks/pre-commit-doctor.sh +0 -0
- package/hooks/run-hook.cmd +0 -0
- package/hooks/session-start.sh +5 -5
- package/package.json +3 -1
- package/references/collaborative-protocol.md +50 -0
- package/references/gd-style.md +121 -0
- package/references/godot-version.md +51 -0
- package/references/mcp-tools-reference.md +199 -0
- package/references/public-surface-language.md +30 -0
- package/references/summer-folder.md +90 -0
- package/references/template-registry.md +73 -0
- package/skills/2d-assets/character-portrait/SKILL.md +17 -12
- package/skills/2d-assets/concept-art/SKILL.md +10 -9
- package/skills/2d-assets/pixel-art/SKILL.md +36 -20
- package/skills/2d-assets/skybox-panorama/SKILL.md +32 -37
- package/skills/2d-assets/sprite-sheet/SKILL.md +40 -35
- package/skills/2d-assets/tileable-texture/SKILL.md +28 -22
- package/skills/2d-assets/ui-graphics/SKILL.md +46 -32
- package/skills/2d-assets/use-widget-asset/SKILL.md +8 -6
- package/skills/3d-assets/character-model/SKILL.md +34 -19
- package/skills/3d-assets/environment-kit/SKILL.md +11 -5
- package/skills/3d-assets/organic-model/SKILL.md +8 -5
- package/skills/3d-assets/prop-model/SKILL.md +9 -3
- package/skills/3d-assets/vehicle-model/SKILL.md +32 -19
- package/skills/README.md +1 -1
- package/skills/ai-and-npcs/design-npc/SKILL.md +31 -13
- package/skills/animation/animation-tree/SKILL.md +39 -15
- package/skills/animation/facial-and-lipsync/SKILL.md +25 -8
- package/skills/animation/generate-motion/SKILL.md +47 -15
- package/skills/animation/procedural-animation/SKILL.md +31 -13
- package/skills/animation/retarget/SKILL.md +127 -79
- package/skills/audio/adaptive-music/SKILL.md +24 -20
- package/skills/audio/ambient-bed/SKILL.md +22 -16
- package/skills/audio/audio-direction/SKILL.md +15 -10
- package/skills/audio/music-track/SKILL.md +18 -12
- package/skills/audio/sound-effect/SKILL.md +17 -12
- package/skills/audio/voice-line/SKILL.md +42 -42
- package/skills/character-controllers/fps-controller/SKILL.md +26 -16
- package/skills/debugging/debug/SKILL.md +52 -20
- package/skills/deployment/export-and-ship/SKILL.md +105 -38
- package/skills/deployment/remote-deploy/SKILL.md +56 -23
- package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +26 -1
- package/skills/gameplay-mechanics/design-mechanic/SKILL.md +23 -15
- package/skills/level-design/design-level/SKILL.md +22 -8
- package/skills/level-design/scene-to-level/SKILL.md +13 -7
- package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +8 -8
- package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +2 -7
- package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +26 -18
- package/skills/performance/tune-performance/SKILL.md +51 -32
- package/skills/rendering-and-lighting/3d-lighting/SKILL.md +34 -5
- package/skills/rendering-and-lighting/art-direction/SKILL.md +6 -8
- package/skills/scene-and-project/brainstorm-game/SKILL.md +3 -5
- package/skills/scene-and-project/make-game/SKILL.md +2 -1
- package/skills/scene-and-project/play/SKILL.md +10 -4
- package/skills/scene-and-project/scene-composition/SKILL.md +21 -8
- package/skills/scene-and-project/summer-cloud/SKILL.md +1 -1
- package/skills/ui-and-ux/ui-basics/SKILL.md +16 -7
- package/skills/video/animated-loop/SKILL.md +63 -39
- package/skills/video/cinematic-cutscene/SKILL.md +66 -30
- package/skills/video/trailer-shot/SKILL.md +30 -21
- package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +25 -16
- package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +5 -3
- package/skills/visual-effects/game-feel/SKILL.md +51 -33
- package/skills/visual-effects/recipes/dissolve/SKILL.md +51 -9
- package/skills/visual-effects/recipes/fire/SKILL.md +136 -49
- package/skills/visual-effects/recipes/hit-spark/SKILL.md +45 -8
- package/skills/visual-effects/recipes/lightning/SKILL.md +59 -27
- package/skills/visual-effects/recipes/magic-glow/SKILL.md +59 -26
- package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +68 -25
- package/skills/visual-effects/recipes/smoke/SKILL.md +89 -28
- package/skills/visual-effects/recipes/water-ripple/SKILL.md +83 -23
- package/skills/workflow/brainstorming/SKILL.md +1 -1
- package/skills/workflow/brainstorming/scripts/start-server.sh +0 -0
- package/skills/workflow/brainstorming/scripts/stop-server.sh +0 -0
- package/skills/workflow/debugging-game-feel/SKILL.md +29 -9
- package/skills/workflow/diagnosing-perf-regressions/SKILL.md +46 -21
- package/skills/workflow/gameskill/SKILL.md +11 -2
- package/skills/workflow/headless-scripting/SKILL.md +407 -0
- package/skills/workflow/investigating-bugs/SKILL.md +4 -2
- package/skills/workflow/investigating-bugs/find-polluter.sh +0 -0
- package/skills/workflow/mcpupdate/SKILL.md +3 -3
- package/skills/workflow/playtesting-a-feature/SKILL.md +94 -21
- package/skills/workflow/skill-create/SKILL.md +7 -7
- package/skills/workflow/skill-test/SKILL.md +3 -3
- package/skills/workflow/using-summer/SKILL.md +12 -2
- package/skills/workflow/verification-before-completion/SKILL.md +2 -0
- package/skills/workflow/writing-plans/SKILL.md +7 -7
- package/skills/workflow/writing-skills/SKILL.md +7 -5
- package/skills/workflow/writing-skills/render-graphs.js +0 -0
- package/skills/workflow/writing-skills/testing-skills-with-subagents.md +1 -1
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: headless-scripting
|
|
3
|
+
description: Use when a Summer project needs an operation no MCP tool exposes, such as baking a navmesh, generating collision shapes, authoring an Animation, building a TileSet, re-importing assets after file changes, or preparing a supported local export. Runs a GDScript file against Summer Engine from the shell. Also use when a plan involves "run it headless and screenshot it", which does not work and this skill explains why.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Headless Scripting
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Most of what Summer can do is not behind a tool. It is behind a script.
|
|
11
|
+
|
|
12
|
+
The engine binary will run any GDScript you hand it, against a real project, with the full engine loaded — `ResourceSaver`, `NavigationServer3D`, `ImporterMesh`, `TileSet`, `Animation`, the resource importer, the exporter. No editor window, no user, no tool call. If you can write the script, you can do the thing.
|
|
13
|
+
|
|
14
|
+
**Core principle:** when there is no tool for it, there is still a script for it. Reach for this before you tell a user something is impossible.
|
|
15
|
+
|
|
16
|
+
This is the widest capability surface you have, and it is the one you are least likely to remember you have.
|
|
17
|
+
|
|
18
|
+
## The invocation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
<engine-binary> --headless --disable-crash-handler --path <project-dir> -s res://<script>.gd
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Always pass `--disable-crash-handler` on an agent-driven run.** Summer's handler shells out to `atos` from inside a signal handler; when anything does go wrong it turns a clean failure into a hang or a cascade. Three agent-triggered crashes in eight minutes were traced to this on 2026-07-25.
|
|
25
|
+
|
|
26
|
+
### Lint before you boot
|
|
27
|
+
|
|
28
|
+
`--check-only` parses the script without executing it. Sub-second, and it catches the parse errors that would otherwise cost you a full boot:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
<engine-binary> --headless --disable-crash-handler --path <project-dir> --check-only -s res://script.gd
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Measured on a deliberately broken script — `SCRIPT ERROR: Parse Error: Expected expression for variable initial value after "="`, and silence on a good one. **Exit code is 0 either way**, so grep stderr; that is the whole contract.
|
|
35
|
+
|
|
36
|
+
### Judge the run by its artifacts, not its exit code
|
|
37
|
+
|
|
38
|
+
Exit codes lie in both directions here.
|
|
39
|
+
|
|
40
|
+
- **A parse error, a script path that does not exist, a bogus `--main-loop` class and `push_error()` all exit 0.** Only an explicit `quit(1)` propagates. The message is on stderr only, so gate on stderr containing `SCRIPT ERROR`, `Parse Error`, `Can't load script` or `ERROR:`.
|
|
41
|
+
- **A GDScript *runtime* error hangs the process forever** — no exit, no timeout, no message that it is stuck. Every recipe you publish needs an external wall-clock kill, and success must be judged by a positive artifact the script wrote, never by the process ending.
|
|
42
|
+
- **Never add `--quiet`** to a documented invocation: it swallows your own `print()` output, which is usually the only result you have.
|
|
43
|
+
- `WARNING: ObjectDB instances leaked at exit` prints on **every** successful run. Never gate on it.
|
|
44
|
+
- **A SIGABRT *after* the work landed is a teardown artifact, not a failure.** Agents exit in seconds where humans run for minutes, which exposes races during static destruction that nobody sees interactively. Check the file you expected on disk before concluding anything, and never blind-retry a crash — that is how one crash became three.
|
|
45
|
+
|
|
46
|
+
### Two invocations to never issue
|
|
47
|
+
|
|
48
|
+
- **`--write-movie` under `--headless` is a deterministic crash**, not a graceful failure. `MovieWriter::add_frame()` dereferences a null image from the dummy renderer on frame one. Headless has no pixels, so there is no safe form of this. Frame capture needs the windowed verify instance below.
|
|
49
|
+
- **`--quit-after 1`** hits the fast-exit path and aborts during static destruction. Use `--headless --quit-after 60` if you need a bounded editor run — with `--headless`, never without.
|
|
50
|
+
|
|
51
|
+
### Never put a window on the user's screen
|
|
52
|
+
|
|
53
|
+
<EXTREMELY-IMPORTANT>
|
|
54
|
+
**Every engine launch you make is `--headless`.** The user did not ask you to open anything. A window that appears unasked steals focus, interrupts whatever they were doing, and is indistinguishable from a crash or a bug from their side. This is a product rule, not a convenience.
|
|
55
|
+
|
|
56
|
+
The one exception is the verify instance, and it is safe precisely because it is not visible: `--summer-verify` opens a real rendering window that is **hidden, `NO_FOCUS`, and positioned at `(-32000,-32000)`** (`main/main.cpp:2363` and following). That is why it can produce real pixels without ever appearing. Never reproduce the "I need a renderer" reasoning by launching a plain windowed editor — use `--summer-verify`.
|
|
57
|
+
|
|
58
|
+
If you genuinely believe the user needs to see the editor, ask them first.
|
|
59
|
+
</EXTREMELY-IMPORTANT>
|
|
60
|
+
|
|
61
|
+
### Finding the binary — do not guess
|
|
62
|
+
|
|
63
|
+
Call `summer_get_project_context` and read **`engineBinaryPath`**. That is the running engine's own `OS.get_executable_path()`, already resolved through the macOS `.app` bundle to the real inner executable.
|
|
64
|
+
|
|
65
|
+
If you are inside a script that is already running in the engine, it is just `OS.get_executable_path()`.
|
|
66
|
+
|
|
67
|
+
If neither is available (an older engine build that predates `engineBinaryPath`, or no editor running), fall back to the install location for the platform:
|
|
68
|
+
|
|
69
|
+
| Platform | Path |
|
|
70
|
+
|---|---|
|
|
71
|
+
| macOS | `/Applications/Summer.app/Contents/MacOS/Summer` |
|
|
72
|
+
| Windows | `%LOCALAPPDATA%\Summer\current\Summer.exe` |
|
|
73
|
+
| Linux | wherever the AppImage/binary was installed |
|
|
74
|
+
|
|
75
|
+
<EXTREMELY-IMPORTANT>
|
|
76
|
+
There is no `godot` binary on a Summer user's machine. `godot --headless`, `godot4`, `/usr/local/bin/godot` — none of these exist. A command built on that name fails with "command not found" on every single user, and no amount of retrying changes it. This skill exists partly because that exact instruction shipped in our own docs for months.
|
|
77
|
+
</EXTREMELY-IMPORTANT>
|
|
78
|
+
|
|
79
|
+
### The script must extend `SceneTree` or `MainLoop`
|
|
80
|
+
|
|
81
|
+
`-s` does not load an arbitrary script. `main/main.cpp` instantiates it as the process's main loop, and rejects anything that is not one.
|
|
82
|
+
|
|
83
|
+
```gdscript
|
|
84
|
+
extends SceneTree
|
|
85
|
+
|
|
86
|
+
func _initialize():
|
|
87
|
+
# your work here
|
|
88
|
+
quit() # or the process runs forever
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`_initialize()` runs before the first frame. If you need frames to have elapsed — physics settled, nodes `_ready`-ed — `await process_frame` inside `_initialize()`, or use `_process(delta)` and `quit()` when done.
|
|
92
|
+
|
|
93
|
+
Always call `quit()`. There is no other exit.
|
|
94
|
+
|
|
95
|
+
**The trap:** a script extending `Node` (the reflex, since almost all GDScript does) is a wedge, not an error. The engine's rejection path calls `OS.alert()`, and on macOS that is an `NSAlert` `runModal` with **no headless guard** (`platform/macos/os_macos.mm:352-369`). The process blocks forever on a modal dialog that has no window to appear in. You get **no output at all** and a hung process that must be killed. Measured: five orphaned processes accumulated in one session before the cause was found.
|
|
96
|
+
|
|
97
|
+
If a headless run produces zero output and never returns, check the first line of your script before you check anything else.
|
|
98
|
+
|
|
99
|
+
## What this unlocks
|
|
100
|
+
|
|
101
|
+
Every one of these was run against the shipped binary. The result column is measured output, not expectation.
|
|
102
|
+
|
|
103
|
+
| Task | Result |
|
|
104
|
+
|---|---|
|
|
105
|
+
| Collision shape from a mesh | `create_trimesh_shape()` → 12 faces; `create_convex_shape()` → 8 points |
|
|
106
|
+
| Navmesh bake | `NavigationServer3D.bake_from_source_geometry_data()` → 2 polys / 4 verts, saved to `.tres` |
|
|
107
|
+
| LOD generation | `ImporterMesh` instantiable, `generate_lods()` callable |
|
|
108
|
+
| Animation authoring | value + bezier + method tracks saved and reloaded with all 3 intact |
|
|
109
|
+
| TileSet authoring | atlas source + per-tile collision polygon, saved to `.tres` |
|
|
110
|
+
| Asset re-import | `--import` produced the `.import` sidecar and the `.ctex` |
|
|
111
|
+
| Full export | hand-written `export_presets.cfg` + `--export-release` → a 144 MB runnable `.app` |
|
|
112
|
+
|
|
113
|
+
Themes, shaders, audio bus layouts, input maps and project settings are the same story: plain resources, authored and saved from a script.
|
|
114
|
+
|
|
115
|
+
### Bake a navmesh
|
|
116
|
+
|
|
117
|
+
```gdscript
|
|
118
|
+
extends SceneTree
|
|
119
|
+
|
|
120
|
+
func _initialize():
|
|
121
|
+
var nav := NavigationMesh.new()
|
|
122
|
+
nav.cell_size = 0.25
|
|
123
|
+
nav.agent_radius = 0.5
|
|
124
|
+
|
|
125
|
+
var geo := NavigationMeshSourceGeometryData3D.new()
|
|
126
|
+
var ground := PlaneMesh.new()
|
|
127
|
+
ground.size = Vector2(10, 10)
|
|
128
|
+
geo.add_mesh(ground, Transform3D.IDENTITY)
|
|
129
|
+
|
|
130
|
+
NavigationServer3D.bake_from_source_geometry_data(nav, geo)
|
|
131
|
+
print("polys=", nav.get_polygon_count(), " verts=", nav.vertices.size())
|
|
132
|
+
ResourceSaver.save(nav, "res://levels/level1_nav.tres")
|
|
133
|
+
quit()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`ResourceSaver.save()` returns `0` (`OK`) on success. Check it — a silent non-zero is how you ship a level with no navmesh.
|
|
137
|
+
|
|
138
|
+
Prefer parsing collision shapes over visual meshes as source geometry. `add_mesh()` on a visual mesh pulls geometry back off the GPU and warns loudly about it.
|
|
139
|
+
|
|
140
|
+
### Generate a collision shape
|
|
141
|
+
|
|
142
|
+
```gdscript
|
|
143
|
+
var mesh: Mesh = load("res://models/rock.glb").instantiate().get_child(0).mesh
|
|
144
|
+
ResourceSaver.save(mesh.create_trimesh_shape(), "res://models/rock_col.tres") # static
|
|
145
|
+
ResourceSaver.save(mesh.create_convex_shape(), "res://models/rock_cvx.tres") # dynamic
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Trimesh for static geometry, convex for anything that moves — a `RigidBody3D` with a concave shape is a physics bug, not a modelling choice.
|
|
149
|
+
|
|
150
|
+
### Author an Animation
|
|
151
|
+
|
|
152
|
+
```gdscript
|
|
153
|
+
var anim := Animation.new()
|
|
154
|
+
anim.length = 2.0
|
|
155
|
+
|
|
156
|
+
var t := anim.add_track(Animation.TYPE_VALUE)
|
|
157
|
+
anim.track_set_path(t, "Player:position")
|
|
158
|
+
anim.track_insert_key(t, 0.0, Vector3.ZERO)
|
|
159
|
+
anim.track_insert_key(t, 2.0, Vector3(0, 5, 0))
|
|
160
|
+
|
|
161
|
+
var b := anim.add_track(Animation.TYPE_BEZIER)
|
|
162
|
+
anim.track_set_path(b, "Player:modulate:a")
|
|
163
|
+
anim.bezier_track_insert_key(b, 0.0, 1.0, Vector2(-0.2, 0), Vector2(0.2, 0))
|
|
164
|
+
|
|
165
|
+
var m := anim.add_track(Animation.TYPE_METHOD)
|
|
166
|
+
anim.track_set_path(m, "Player")
|
|
167
|
+
anim.track_insert_key(m, 1.0, {"method": "on_landed", "args": []})
|
|
168
|
+
|
|
169
|
+
ResourceSaver.save(anim, "res://animations/rise.tres")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Track paths are `NodePath:property` relative to the `AnimationPlayer`'s `root_node`. Getting that path wrong is the usual reason an animation loads fine and animates nothing.
|
|
173
|
+
|
|
174
|
+
### Build a TileSet with collision
|
|
175
|
+
|
|
176
|
+
```gdscript
|
|
177
|
+
var ts := TileSet.new()
|
|
178
|
+
ts.tile_size = Vector2i(16, 16)
|
|
179
|
+
ts.add_physics_layer()
|
|
180
|
+
|
|
181
|
+
var atlas := TileSetAtlasSource.new()
|
|
182
|
+
atlas.texture = load("res://art/tiles.png")
|
|
183
|
+
atlas.texture_region_size = Vector2i(16, 16)
|
|
184
|
+
atlas.create_tile(Vector2i(0, 0))
|
|
185
|
+
ts.add_source(atlas)
|
|
186
|
+
|
|
187
|
+
var tile := atlas.get_tile_data(Vector2i(0, 0), 0)
|
|
188
|
+
tile.add_collision_polygon(0)
|
|
189
|
+
tile.set_collision_polygon_points(0, 0, PackedVector2Array([
|
|
190
|
+
Vector2(-8, -8), Vector2(8, -8), Vector2(8, 8), Vector2(-8, 8)
|
|
191
|
+
]))
|
|
192
|
+
|
|
193
|
+
ResourceSaver.save(ts, "res://levels/tiles.tres")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`add_physics_layer()` returns `void`, not the layer index. Layers are indexed in creation order from 0.
|
|
197
|
+
|
|
198
|
+
### Build a scene from script
|
|
199
|
+
|
|
200
|
+
`PackedScene.pack()` + `ResourceSaver.save()` is how you write a `.tscn` a script built. It has one trap that costs an entire run:
|
|
201
|
+
|
|
202
|
+
<EXTREMELY-IMPORTANT>
|
|
203
|
+
**`pack()` silently drops every node whose `owner` is not set**, and both `pack()` and `ResourceSaver.save()` return `0` anyway. You get two success codes and a scene missing most of its contents.
|
|
204
|
+
|
|
205
|
+
Measured — a root with one owned child, one unowned sibling, and an unowned grandchild under the owned one:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
PACK err=0
|
|
209
|
+
SAVE err=0
|
|
210
|
+
RELOADED children=[&"Owned"] <- both unowned nodes gone, no error anywhere
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Note the grandchild vanished too: being a descendant of an owned node is **not** enough. Every node you want persisted needs `.owner` set to the scene root. The root itself does not need one.
|
|
214
|
+
</EXTREMELY-IMPORTANT>
|
|
215
|
+
|
|
216
|
+
```gdscript
|
|
217
|
+
var root := Node2D.new()
|
|
218
|
+
|
|
219
|
+
var layer := TileMapLayer.new()
|
|
220
|
+
layer.tile_set = ts
|
|
221
|
+
root.add_child(layer)
|
|
222
|
+
layer.owner = root # omit this and the layer vanishes, silently
|
|
223
|
+
|
|
224
|
+
var ps := PackedScene.new()
|
|
225
|
+
ps.pack(root)
|
|
226
|
+
ResourceSaver.save(ps, "res://levels/level1.tscn")
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
This is also the answer for tilemaps specifically. A `TileMapLayer`'s cells serialise into `tile_map_data`, a `PackedByteArray` — so "just hand-write the `.tscn`" stops being true the moment there are cells in it. Use `set_cell()` and pack the scene; do not try to edit that field as text.
|
|
230
|
+
|
|
231
|
+
### Re-import assets you wrote to disk
|
|
232
|
+
|
|
233
|
+
**This is the single most common way an agent breaks a project.** A `.png` written directly to disk is not a resource yet:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
ERROR: No loader found for resource: res://raw.png (expected type: unknown)
|
|
237
|
+
at: _load (core/io/resource_loader.cpp:358)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The file exists. `FileAccess.file_exists()` returns `true`. `load()` fails anyway, because there is no `.import` sidecar and no imported `.ctex` in `.godot/imported/`.
|
|
241
|
+
|
|
242
|
+
### Prefer the routes that need no import at all
|
|
243
|
+
|
|
244
|
+
Reach for these before reaching for `--import`:
|
|
245
|
+
|
|
246
|
+
1. **`ResourceSaver.save()` to `.tres` / `.res`.** No import step, no editor, no `.godot/` race, and it ships correctly in an exported build. Round-trips for `ImageTexture`, `AudioStreamWAV`, `AudioStreamOggVorbis`, `AudioStreamMP3`, `FontFile`, `ArrayMesh`, `Translation`, `AudioBusLayout`. (One trap: `PortableCompressedTexture2D` saves with `err 0` and reloads **empty** — use `ImageTexture`.)
|
|
247
|
+
2. **`.dds` and `.po` load raw.** They are the only formats that need no `.import` and ship as-is. A DDS is a 128-byte header plus raw pixels, which a script can emit directly.
|
|
248
|
+
3. **`summer_import_from_url`** already does its own rescan-and-settle in-process, so assets that arrive that way are registered without any of this.
|
|
249
|
+
|
|
250
|
+
Only when none of those fit — you genuinely need a `.png` on disk to become a texture — run the import:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
<engine-binary> --headless --disable-crash-handler --path <project-dir> --import
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Judge it by the `.import` sidecar and the file in `.godot/imported/`, not by the exit status.** If those exist the import succeeded, however the process died. Do not retry blindly. Measured 3/3 clean on 4.6.1; a wider sample put `--import` aborts at 2/12 against `--editor`'s 3/50, statistically the same background boot-abort rate rather than an `--import`-specific crash.
|
|
257
|
+
|
|
258
|
+
<EXTREMELY-IMPORTANT>
|
|
259
|
+
**On shipped builds up to 0.5.55, `--import` hijacks the user's editor connection.** It starts the local API server, which unconditionally overwrites `~/.summer/api-port` and mints a fresh `~/.summer/api-token` — machine-global, no project identity, last writer wins. When the import process exits, that pointer is left aimed at a **dead port with a dead token**, so the user's editor becomes unreachable and MCP reports "Summer Engine is not running" while their editor is plainly open.
|
|
260
|
+
|
|
261
|
+
Measured on 0.5.55: `port 6550 -> 6551`, new token, editor still alive on 6550 and now unreachable.
|
|
262
|
+
|
|
263
|
+
So on a shipped build: run `--import` against a throwaway copy where you can, warn the user before running it against their open project, and tell them to restart the editor afterwards if their agent connection drops.
|
|
264
|
+
|
|
265
|
+
**Fixed on `main` but not yet in any shipped binary** (`_should_publish_discovery`, commit `420222554e`; tracked as SUM-161): batch-mode invocations — `--import`, `--summer-verify`, `--export-*`, `-s`, `--headless`, `--quit-after` — no longer publish. Once that ships, this warning applies only to older builds. Plain `--headless -s` never started the server on any build, so everything else in this skill is unaffected.
|
|
266
|
+
</EXTREMELY-IMPORTANT>
|
|
267
|
+
|
|
268
|
+
Two lifecycle facts that decide the shape of every headless run:
|
|
269
|
+
|
|
270
|
+
- **There is a hard work-completion floor between 3 and 5 seconds.** Fresh project, 8 small PNGs, editor killed at N seconds: at **3s, 0/8 imports landed in 10/10 cycles** — it boots, gets killed mid-scan, and accomplishes nothing. At **5s, 8/8 landed every cycle.** Never give an import a 3-second budget.
|
|
271
|
+
- **`--headless --editor` is not a filesystem watcher.** It never notices files created after boot — 6 assets added 12s in, 45s wait, 0 imported. The rescan is driven by `NOTIFICATION_APPLICATION_FOCUS_IN` (`editor_node.cpp:1096`) and headless never fires a focus event. A long-lived headless editor serves a boot-time snapshot forever.
|
|
272
|
+
|
|
273
|
+
Both ends of the lifecycle are broken, differently: short runs do no work, long ones go stale. **Boot → act → exit is the only shape that works today.**
|
|
274
|
+
|
|
275
|
+
Measured before and after on the same file:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
FILE_EXISTS=true IMPORT_SIDECAR=false → ERROR: No loader found for resource
|
|
279
|
+
FILE_EXISTS=true IMPORT_SIDECAR=true → LOAD_RESULT=<CompressedTexture2D>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Run `--import` after **any** batch of raw asset files you wrote yourself — textures, audio, models. The MCP's `summer_import_from_url` already does its own rescan-and-settle, so assets that arrive that way are fine; assets you wrote with `summer_write_file` or your own shell are not.
|
|
283
|
+
|
|
284
|
+
### Export a build
|
|
285
|
+
|
|
286
|
+
`export_presets.cfg` is plain text you can write. Then:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
<engine-binary> --headless --path <project-dir> --export-release "macOS" <out-dir>/Game.app
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Exit code 0 and a real bundle on disk are the success condition — check both. Export templates for the engine's exact version must be installed, or the export fails.
|
|
293
|
+
|
|
294
|
+
## The trap: `--headless` has no pixels
|
|
295
|
+
|
|
296
|
+
<EXTREMELY-IMPORTANT>
|
|
297
|
+
`--headless` loads the dummy rendering driver. `viewport.get_texture().get_image()` returns **null**:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
ERROR: Parameter "t" is null.
|
|
301
|
+
at: texture_2d_get (servers/rendering/dummy/storage/texture_storage.h:106)
|
|
302
|
+
GET_IMAGE=<Object#null>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Any plan of the form "run the game headless and screenshot it" is dead on arrival.** It does not error usefully — it hands you a null and continues.
|
|
306
|
+
|
|
307
|
+
Performance is equally meaningless there, but **not in the way it first looks.** Sampled every 30 frames across a 401-frame headless run of the same scene:
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
fps=1.0 draw_calls=0.0 objects=0.0 <- first ~1s
|
|
311
|
+
fps=117.0 draw_calls=0.0 objects=0.0
|
|
312
|
+
fps=145.0 draw_calls=0.0 objects=0.0
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Two separate traps in that table:
|
|
316
|
+
|
|
317
|
+
1. **`draw_calls` and `objects` are `0.0` at every sample, forever.** That is the real headless signature — there is no renderer to count. Those monitors never become meaningful.
|
|
318
|
+
2. **The FPS number is not 1.0, and it is not real either.** It settles high (145 here) precisely *because* nothing is being drawn. Quoting it as the game's frame rate is worse than quoting nothing.
|
|
319
|
+
|
|
320
|
+
And a third trap that is **not** headless-specific: `TIME_FPS` reads exactly `1.0` until it has about a second of history. A probe that samples too early reports 1 fps in a windowed instance too. A leading `1.0` is warm-up, not a stall — sample for at least a second before believing any frame rate.
|
|
321
|
+
</EXTREMELY-IMPORTANT>
|
|
322
|
+
|
|
323
|
+
The answer is not headless. The engine ships a second mode: an offscreen **windowed** instance with a real renderer, positioned far off-screen so the user never sees it. That is what `RunVerification` spawns, and you can spawn it directly:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
<engine-binary> --path <project-dir> \
|
|
327
|
+
--summer-verify <abs-path-to-probe.gd> \
|
|
328
|
+
--summer-verify-out <abs-out-dir> \
|
|
329
|
+
--summer-verify-max 30
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Note: **no `--headless`**. That is the entire difference.
|
|
333
|
+
|
|
334
|
+
Measured in that instance, same project, same machine:
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
viewport_image_null : false (1152x648, pixel(4,4) = the real rendered colour)
|
|
338
|
+
display_server : macOS (Metal 4.0 — Forward+)
|
|
339
|
+
draw_calls : 1.0 (vs 0.0 headless, at every sample)
|
|
340
|
+
frames saved : 00_start.jpg 01_after_walk.jpg 02_after_jump.jpg
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
So: **scripting and authoring go through `--headless`. Seeing and measuring go through the verify instance.** Choose on that axis and you will not get a null.
|
|
344
|
+
|
|
345
|
+
For writing probes — the `SummerProbeBase` API, the coroutine rules, what is and is not reproducible — see `summer:playtesting-a-feature`.
|
|
346
|
+
|
|
347
|
+
## What a script genuinely cannot do
|
|
348
|
+
|
|
349
|
+
Short list, and it is short on purpose. Everything not on it is worth attempting.
|
|
350
|
+
|
|
351
|
+
- **Lightmap baking.** `LightmapGI::bake` is not runtime-callable. Editor process only.
|
|
352
|
+
- **Occlusion culling bake.** `OccluderInstance3D.bake_single_node`, same.
|
|
353
|
+
- **Anything needing `EditorInterface`.** Outside the editor, `ClassDB.can_instantiate("EditorScript")` is `false`.
|
|
354
|
+
**Trap:** `Engine.has_singleton("EditorInterface")` returns **`true`** even in a plain headless script — it lies. Retrieving the singleton then fails with `Can't retrieve singleton 'EditorInterface' outside of editor`. Test with `ClassDB.can_instantiate("EditorScript")`, which tells the truth.
|
|
355
|
+
|
|
356
|
+
## Never verify a write by reading it back from the thing you wrote
|
|
357
|
+
|
|
358
|
+
This is the single highest-yield rule in this document, because it is the failure mode with no symptom. In-process read-back returns what you set, from memory, whether or not anything reached disk or took effect. The check passes, the work is gone.
|
|
359
|
+
|
|
360
|
+
Four measured instances of the same shape:
|
|
361
|
+
|
|
362
|
+
| What you did | What reading it back says | What is actually true |
|
|
363
|
+
|---|---|---|
|
|
364
|
+
| `PackedScene.pack()` with unset `owner`s | `pack` 0, `save` 0 | Those nodes are not in the file |
|
|
365
|
+
| `ProjectSettings.set_setting()` without `save()` | the new value | Nothing on disk; next boot has the old one |
|
|
366
|
+
| A setting with a feature-tag override | the base value | The engine is using the override |
|
|
367
|
+
| `PathFollow2D.progress = 50.0` | `50.0` | The node never moved |
|
|
368
|
+
|
|
369
|
+
**Verify from a second process, or by re-reading the file from disk.** Cheapest reliable form: finish the script, then run a second one-liner that `load()`s the artefact fresh and prints what it found — which is exactly what the measurements in this document do. `ResourceSaver.save()` returning `0` means "the write was attempted without error", not "the thing you meant is in there".
|
|
370
|
+
|
|
371
|
+
## Working pattern
|
|
372
|
+
|
|
373
|
+
1. Write the script with `summer_write_file` (or your own shell) into the project.
|
|
374
|
+
2. Get the binary from `summer_get_project_context` → `engineBinaryPath`.
|
|
375
|
+
3. Run it. **Read stdout** — GDScript errors go there, and a script that failed to parse still exits cleanly-looking.
|
|
376
|
+
4. Verify the artifact exists on disk and check the `ResourceSaver.save()` return value. "The script ran" is not "the resource was written".
|
|
377
|
+
5. If you wrote raw asset files, run `--import`.
|
|
378
|
+
6. Delete throwaway scripts. Do not leave `probe_temp_3.gd` in a user's project.
|
|
379
|
+
|
|
380
|
+
Keep scripts in the project (`res://` is required for `-s`), but name them so their purpose is obvious and clean up after yourself.
|
|
381
|
+
|
|
382
|
+
## Red Flags — STOP
|
|
383
|
+
|
|
384
|
+
| Red flag | Reality |
|
|
385
|
+
|---|---|
|
|
386
|
+
| `godot --headless ...` | That binary does not exist on any user machine. Use `engineBinaryPath`. |
|
|
387
|
+
| A `-s` script that `extends Node` | Silent hang on an invisible modal. Must be `SceneTree` or `MainLoop`. |
|
|
388
|
+
| Headless run returned nothing and never exited | You extended the wrong class. Kill it and fix line 1. |
|
|
389
|
+
| "Run it headless and screenshot" | Headless has no pixels. Use the verify instance, without `--headless`. |
|
|
390
|
+
| Profiling under `--headless` | `draw_calls`/`objects` are 0.0 forever; FPS is inflated by drawing nothing. Use the verify instance. |
|
|
391
|
+
| Quoting a frame rate sampled in the first second | `TIME_FPS` reads 1.0 until it warms up, in every mode. |
|
|
392
|
+
| Wrote a `.png`, then `load()`ed it | No loader found. Save a `.tres` instead, or run `--import`. |
|
|
393
|
+
| `--import` against the user's open project on a shipped build | Hijacks their editor binding. Warn first. |
|
|
394
|
+
| Killing an import at 3s | Measured 0/8 assets registered. The floor is ~5s. |
|
|
395
|
+
| A long-lived `--headless --editor` as a file watcher | It never rescans. Boot, act, exit. |
|
|
396
|
+
| Adding `--quiet` | Swallows your own print() output. |
|
|
397
|
+
| Launching the editor without `--headless` | Pops a window onto the user's screen unasked. |
|
|
398
|
+
| Ignoring `ResourceSaver.save()`'s return value | Non-zero is a silent failure that ships. |
|
|
399
|
+
| `pack()` without setting `.owner` on every node | Two success codes, nodes missing from the file. |
|
|
400
|
+
| Verifying a write by reading the object you just wrote | In-memory read-back passes whether or not it landed. |
|
|
401
|
+
| Script with no `quit()` | Runs forever. |
|
|
402
|
+
| "There's no tool for that, so it can't be done" | There is a script for it. Write the script. |
|
|
403
|
+
|
|
404
|
+
**Related skills:**
|
|
405
|
+
- `summer:playtesting-a-feature` — the verify instance, probes, and proving gameplay behaviour.
|
|
406
|
+
- `summer:export-and-ship` — the full release pipeline this skill's export example is one step of.
|
|
407
|
+
- `summer:verification-before-completion` — checking the artifact before claiming done.
|
|
@@ -176,7 +176,8 @@ You MUST complete each phase before proceeding to the next.
|
|
|
176
176
|
- Automated test if possible
|
|
177
177
|
- One-off test script if no framework
|
|
178
178
|
- MUST have before fixing
|
|
179
|
-
-
|
|
179
|
+
- Verify it red-green: run the test before the fix and confirm it FAILS for the stated reason. A test you never saw fail proves nothing.
|
|
180
|
+
- For a runtime game bug, the reproduction is a `RunVerification` probe: press the input, assert the state. Re-run the same probe after the fix.
|
|
180
181
|
|
|
181
182
|
2. **Implement Single Fix**
|
|
182
183
|
- Address the root cause identified
|
|
@@ -284,8 +285,9 @@ These techniques are part of systematic debugging and available in this director
|
|
|
284
285
|
- **`condition-based-waiting.md`** - Replace arbitrary timeouts with condition polling
|
|
285
286
|
|
|
286
287
|
**Related skills:**
|
|
287
|
-
- **summer:test-driven-development** - For creating failing test case (Phase 4, Step 1)
|
|
288
288
|
- **summer:verification-before-completion** - Verify fix worked before claiming success
|
|
289
|
+
- **summer:debug** - The Summer-specific triage loop (script errors → console → debugger → probe)
|
|
290
|
+
- **summer:playtesting-a-feature** - When the fix is a gameplay behaviour that has to be walked
|
|
289
291
|
|
|
290
292
|
## Real-World Impact
|
|
291
293
|
|
|
File without changes
|
|
@@ -16,7 +16,7 @@ This skill lives in the **summer-cli** repo (the CLI + MCP + skills), normally a
|
|
|
16
16
|
`<SummerEngine checkout>/tools/summer-cli/`. Paths below are relative to that repo root.
|
|
17
17
|
|
|
18
18
|
- `src/mcp/tools/*.ts`: the `summer_*` MCP tool definitions ("ops": scene/asset/project/etc.)
|
|
19
|
-
-
|
|
19
|
+
- `../../../references/mcp-tools-reference.md`: human-readable tool reference
|
|
20
20
|
- `src/mcp/tools/project-tools.ts` + `docs/MCP_FRESH_CHAT_PLAYBOOK.md`: the **agent playbook** returned by `summer_get_agent_playbook` (startup flow / defaults that steer agents)
|
|
21
21
|
- `src/lib/skills-registry.ts`: how skills are discovered/surfaced to the agent
|
|
22
22
|
- `skills/<category>/<name>/SKILL.md`: the **prompt-engineering skills** (e.g. `character-controllers/fps-controller`, `scene-and-project/make-game`, `level-design/design-level`, `visual-effects/*`)
|
|
@@ -53,7 +53,7 @@ node -e "import('./dist/lib/skills-registry.js').then(m=>{const r=m.SKILL_REGIST
|
|
|
53
53
|
- `.codex-plugin/plugin.json`
|
|
54
54
|
- `AGENTS.md`
|
|
55
55
|
- `GEMINI.md`
|
|
56
|
-
-
|
|
56
|
+
- `../../../references/mcp-tools-reference.md`
|
|
57
57
|
- `docs/DEVELOPMENT.md`
|
|
58
58
|
- `docs/SKILLS.md`
|
|
59
59
|
|
|
@@ -75,7 +75,7 @@ node -e "import('./dist/lib/skills-registry.js').then(m=>{const r=m.SKILL_REGIST
|
|
|
75
75
|
2. **Open the exact file from the map above**. Do not scan the tree.
|
|
76
76
|
3. **Make the change** using the existing maintenance skills:
|
|
77
77
|
- New skill: `skills/workflow/skill-create`. Edit/append: `skills/workflow/skill-improve`. Game-dev win: `skills/workflow/gameskill`.
|
|
78
|
-
- Tool behavior / playbook: edit `src/mcp/tools/*.ts` and/or `docs/MCP_FRESH_CHAT_PLAYBOOK.md`; keep
|
|
78
|
+
- Tool behavior / playbook: edit `src/mcp/tools/*.ts` and/or `docs/MCP_FRESH_CHAT_PLAYBOOK.md`; keep `../../../references/mcp-tools-reference.md` in sync. Check `src/mcp/tools/project-tools.test.ts` still passes.
|
|
79
79
|
- Public-facing change: update the matching file in the `docs` repo.
|
|
80
80
|
4. **Keep it concrete**. Every captured pattern gets a real, runnable code block (below), not prose.
|
|
81
81
|
5. **Verify + commit.** If you touched `src/`, run `npm test`. Commit on a branch and summarize.
|
|
@@ -29,12 +29,9 @@ Static analysis is necessary but never sufficient for gameplay features. A clean
|
|
|
29
29
|
NO "FEATURE SHIPPED" CLAIM WITHOUT A PLAYED WALKTHROUGH
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
If you have not, in this session,
|
|
32
|
+
If you have not, in this session, driven the feature and read back what happened, you cannot claim it works.
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
2. Asked the user to walk it and confirmed the result via `summer_get_console` / `summer_get_debugger_errors`
|
|
36
|
-
|
|
37
|
-
— then you cannot claim the feature works.
|
|
34
|
+
**You walk the feature. Not the user.** A `RunVerification` probe presses your game's real inputs, reads state back across frames, and saves real rendered frames. Handing the walkthrough to the user is a last resort for things a probe genuinely cannot judge — see [What only a human can answer](#what-only-a-human-can-answer) — not the default.
|
|
38
35
|
|
|
39
36
|
## The Loop
|
|
40
37
|
|
|
@@ -51,7 +48,57 @@ Before pressing play, write down — in one or two sentences — what "feature w
|
|
|
51
48
|
|
|
52
49
|
If you cannot describe the golden path in one sentence, you do not understand the feature well enough to verify it. Ask the user.
|
|
53
50
|
|
|
54
|
-
### 2.
|
|
51
|
+
### 2. Walk the golden path with a probe
|
|
52
|
+
|
|
53
|
+
Write the golden path as a `RunVerification` probe and send it as a raw op through `summer_batch`. The engine spawns a hidden, disposable game instance positioned offscreen, runs your GDScript, writes `results.json` plus any frames you saved, and kills the instance. It never touches the user's open editor, and nothing appears on their screen.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
summer_batch ops:[{
|
|
57
|
+
"op": "RunVerification",
|
|
58
|
+
"probe_source": "<your GDScript>",
|
|
59
|
+
"max_seconds": 20
|
|
60
|
+
}]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A probe extends `SummerProbeBase` and has: `report(key, value)`, `save_frame(name)`, `dump_tree()`, `press(action, hold_ms)`, `key(keycode, hold_ms)`, `finish()`.
|
|
64
|
+
|
|
65
|
+
```gdscript
|
|
66
|
+
extends SummerProbeBase
|
|
67
|
+
|
|
68
|
+
func _ready() -> void:
|
|
69
|
+
await super._ready()
|
|
70
|
+
for i in 20:
|
|
71
|
+
await get_tree().physics_frame # let the scene settle
|
|
72
|
+
|
|
73
|
+
var player := get_tree().current_scene.get_node("Player")
|
|
74
|
+
var start: Vector2 = player.position
|
|
75
|
+
save_frame("00_start")
|
|
76
|
+
|
|
77
|
+
await press("move_right", 500) # await, or the hold never elapses
|
|
78
|
+
await get_tree().physics_frame
|
|
79
|
+
report("walked_right", player.position.x > start.x + 50)
|
|
80
|
+
save_frame("01_after_walk")
|
|
81
|
+
|
|
82
|
+
var y_before: float = player.position.y
|
|
83
|
+
await press("jump", 50)
|
|
84
|
+
var apex := y_before
|
|
85
|
+
for i in 25:
|
|
86
|
+
await get_tree().physics_frame
|
|
87
|
+
apex = min(apex, player.position.y)
|
|
88
|
+
report("jumped", apex < y_before - 30)
|
|
89
|
+
save_frame("02_after_jump")
|
|
90
|
+
|
|
91
|
+
finish()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Four rules the probe API will punish you for breaking:
|
|
95
|
+
|
|
96
|
+
- **`press()` and `key()` are coroutines.** `await press("jump", 50)` — calling them bare starts the press and never releases it. You also cannot store one in a variable and await it later; it returns void.
|
|
97
|
+
- **Assert on physics-frame-derived state.** `await get_tree().physics_frame` N times, then read positions. That is reproducible run to run.
|
|
98
|
+
- **Assert inequalities, never exact values.** `press(hold_ms)` waits on the wall clock, so the number of physics frames a key is held drifts between runs — the same walk lands on x=245 one run and x=250 the next. `position.x > start.x + 50` is a real assertion; `position.x == 250.0` is a flaky test you wrote yourself.
|
|
99
|
+
- **The global RNG is not seeded.** Anything downstream of `randf()` differs every run. Assert ranges, or seed it yourself in the probe.
|
|
100
|
+
|
|
101
|
+
If the probe cannot express the check — no `SummerProbeBase` in this build, or a genuinely out-of-process feature — fall back to `summer_play` + console reads:
|
|
55
102
|
|
|
56
103
|
```
|
|
57
104
|
summer_clear_console
|
|
@@ -61,14 +108,9 @@ If you cannot describe the golden path in one sentence, you do not understand th
|
|
|
61
108
|
|
|
62
109
|
`summer_clear_console` first so the buffer is not polluted by a previous session's errors. If you skip this, you will chase ghost errors that have nothing to do with the current feature.
|
|
63
110
|
|
|
64
|
-
### 3.
|
|
65
|
-
|
|
66
|
-
The MCP cannot simulate keyboard input, mouse motion, or controller. So either:
|
|
67
|
-
|
|
68
|
-
- **The user plays.** Ask them to walk the exact golden path you defined. Wait for their confirmation that they did. Then check state.
|
|
69
|
-
- **You drive via scene mutation.** For features that can be triggered through `summer_set_prop` or function calls exposed to the editor (e.g. autoload methods), drive them programmatically. Be honest about which path you took.
|
|
111
|
+
### 3. Read back what actually happened
|
|
70
112
|
|
|
71
|
-
|
|
113
|
+
The probe's `results.json` is your primary evidence. Then check the editor's own view:
|
|
72
114
|
|
|
73
115
|
```
|
|
74
116
|
summer_get_console (look for unexpected prints, warnings)
|
|
@@ -76,8 +118,20 @@ After the walkthrough:
|
|
|
76
118
|
summer_get_diagnostics (overall counts)
|
|
77
119
|
```
|
|
78
120
|
|
|
121
|
+
A probe also collects engine errors itself — `results.json` carries `errors_seen`. Read it. An empty `reports` block with `finished: false` means the probe hit its `max_seconds` ceiling before calling `finish()`, which is a failure, not a pass.
|
|
122
|
+
|
|
79
123
|
If anything is non-zero or unexpected, the feature is not done. Go to `summer:debug`.
|
|
80
124
|
|
|
125
|
+
### What only a human can answer
|
|
126
|
+
|
|
127
|
+
A probe reports facts. It cannot hold an opinion. Ask the user only when the question is genuinely experiential:
|
|
128
|
+
|
|
129
|
+
- **Does it feel right?** Floaty jumps, sluggish input, mushy hit feedback — see `summer:debugging-game-feel`.
|
|
130
|
+
- **Does it look good?** Composition, colour, readability. A probe can prove a light exists and prove the frame is not black; it cannot tell you the scene is ugly.
|
|
131
|
+
- **Is this fun?** Not a measurable property.
|
|
132
|
+
|
|
133
|
+
"I cannot simulate input" is not on that list, and has not been true since `RunVerification` shipped. If you catch yourself asking the user to press a key, write the probe instead.
|
|
134
|
+
|
|
81
135
|
### 4. Probe the edges
|
|
82
136
|
|
|
83
137
|
The golden path proves the happy case. Most gameplay bugs live in the edges. Before claiming done, probe at least these four classes:
|
|
@@ -120,13 +174,15 @@ The console catches "looks right but isn't right" cases — silent errors, depre
|
|
|
120
174
|
|
|
121
175
|
## When to record video vs when console+diagnostics is enough
|
|
122
176
|
|
|
123
|
-
|
|
177
|
+
Your probe already produces the visual record: `save_frame()` writes a real rendered JPEG at each step, and a sequence of them across a movement is a flipbook of the feature happening. **You are not the only one who cannot see the game — until you save frames, nobody can.** Save them at the interesting moments and read them back.
|
|
178
|
+
|
|
179
|
+
Ask the user for a video or a live look only when:
|
|
124
180
|
|
|
125
|
-
- The feature is **visual** (camera, lighting, particles, animation, UI layout) and the user is the only one who can see the result.
|
|
126
181
|
- The feature **feels wrong** — see `summer:debugging-game-feel`. Frame counts and console output cannot capture "the jump feels floaty."
|
|
127
|
-
- The bug is **non-deterministic**
|
|
182
|
+
- The bug is **non-deterministic** and your probe cannot reproduce it after several attempts.
|
|
183
|
+
- The judgement is aesthetic — see [What only a human can answer](#what-only-a-human-can-answer).
|
|
128
184
|
|
|
129
|
-
Don't ask
|
|
185
|
+
Don't ask reflexively. A saved frame sequence answers most "did it visually work" questions without involving the user at all.
|
|
130
186
|
|
|
131
187
|
## Red Flags — STOP
|
|
132
188
|
|
|
@@ -137,10 +193,15 @@ Don't ask for video reflexively. Most features can be verified through `summer_g
|
|
|
137
193
|
| "User said make this — I'll move on once it builds" | The user wants the feature to work, not to build. |
|
|
138
194
|
| "It would take too long to play" | Playtest takes 60 seconds. Shipping a broken feature costs hours. |
|
|
139
195
|
| "The unit test passes" | Unit tests cover code paths, not gameplay sequences. |
|
|
140
|
-
| "I already played it in a previous session" | State changed.
|
|
196
|
+
| "I already played it in a previous session" | State changed. Verify in this session. |
|
|
141
197
|
| "I'll let the user catch any issues" | That is the user's job description for a non-AI engineer. Yours is to ship working features. |
|
|
142
198
|
| Skipping `summer_clear_console` before `summer_play` | You will chase last session's errors. |
|
|
143
199
|
| Skipping `summer_get_console` because diagnostics looked fine | Silent warnings and signal misfires hide there. |
|
|
200
|
+
| "The MCP can't simulate input, so the user has to test it" | False. `RunVerification` presses actions and keys. Write the probe. |
|
|
201
|
+
| "I'll ask the user to press jump and tell me what happened" | You can press jump. They should not have to QA your work. |
|
|
202
|
+
| Probe returned `finished: false`, you called it a pass | It hit `max_seconds` before `finish()`. That is a failure. |
|
|
203
|
+
| Asserting `position.x == 250.0` in a probe | Hold time is wall-clock. Assert `> start.x + 50`. |
|
|
204
|
+
| `press("jump", 50)` without `await` | It is a coroutine. The key is never released. |
|
|
144
205
|
|
|
145
206
|
## Rationalization Prevention
|
|
146
207
|
|
|
@@ -154,10 +215,22 @@ Don't ask for video reflexively. Most features can be verified through `summer_g
|
|
|
154
215
|
|
|
155
216
|
## When the engine isn't running
|
|
156
217
|
|
|
157
|
-
|
|
218
|
+
`RunVerification` is dispatched by the editor, so it needs the editor running. **The verify instance itself does not.** You can spawn exactly the same probe runtime straight from your shell, with no editor at all:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
/Applications/Summer.app/Contents/MacOS/Summer \
|
|
222
|
+
--path <project-dir> \
|
|
223
|
+
--summer-verify <abs-path-to-probe.gd> \
|
|
224
|
+
--summer-verify-out <abs-out-dir> \
|
|
225
|
+
--summer-verify-max 30
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Then read `<out-dir>/results.json` and the JPEGs beside it. The probe must extend the base class — copy `summer_probe_base.gd` into the project and `extends "res://summer_probe_base.gd"`, or use the project's `tests/autopilot/` scaffold if it has one. See `summer:headless-scripting` for the full pattern and its traps.
|
|
229
|
+
|
|
230
|
+
So if `summer_play` returns "Summer Engine is not running":
|
|
158
231
|
|
|
159
|
-
1.
|
|
160
|
-
2.
|
|
232
|
+
1. Run the probe directly from the shell as above. That is a real playtest.
|
|
233
|
+
2. Only if that is also impossible: tell the user `summer run` to start the editor, and say plainly "code compiles and looks correct, but I have not played it" — do not claim verification from static analysis alone.
|
|
161
234
|
3. When the engine comes back, run the loop.
|
|
162
235
|
|
|
163
236
|
## The Bottom Line
|