|
| 1 | +# Screenshot-driven testing |
| 2 | + |
| 3 | +Notes for AI agents (and humans) on getting `editor_screenshot` to work the |
| 4 | +first time, instead of looping through "autoload never registered its debugger |
| 5 | +capture within 20s" timeouts. |
| 6 | + |
| 7 | +## Pick the right `source` |
| 8 | + |
| 9 | +| Goal | `source` | Notes | |
| 10 | +|------|----------|-------| |
| 11 | +| Verify in-editor UI / inspector / dock layout | `viewport` | Captures the editor's 2D view directly — no debugger bridge, always works. | |
| 12 | +| Verify a 3D scene framing as the editor camera sees it | `viewport` | Same path, no game subprocess required. | |
| 13 | +| Verify a running game's framebuffer (menus that exist at runtime, gameplay state, particle effects, runtime UI animations) | `game` | Requires the game subprocess + `_mcp_game_helper` autoload + a `project_run`-driven play cycle. | |
| 14 | +| Verify a specific `Camera3D` view without playing | `cinematic` (where supported) | Doesn't require Play. | |
| 15 | + |
| 16 | +**Default to `viewport` when in doubt.** `source="game"` is only the right |
| 17 | +answer when the thing you want to see only exists in the *running* game. |
| 18 | + |
| 19 | +## Recipe: testing a runtime UI option (e.g. main menu) |
| 20 | + |
| 21 | +``` |
| 22 | +1. editor_state -> confirm session, current_scene, readiness="ready" |
| 23 | +2. project_run(mode="current", -> autosave=False so MCP scene mutations stay in memory |
| 24 | + autosave=False) |
| 25 | +3. poll editor_state every ~500ms -> wait until is_playing=true AND game_capture_ready=true |
| 26 | +4. (interact via available write tools as -> e.g. node_set_property, batch_execute, |
| 27 | + needed for the scenario being tested) ui_manage, theme_manage |
| 28 | +5. editor_screenshot(source="game", -> request the capture |
| 29 | + max_resolution=1280) |
| 30 | +6. project_manage(op="stop") -> always stop the run when done |
| 31 | +``` |
| 32 | + |
| 33 | +`game_capture_ready` is the deterministic readiness signal — it flips true |
| 34 | +*only* after the game-side autoload's `mcp:hello` beacon arrives, which means |
| 35 | +the debugger channel is wired up and `mcp:take_screenshot` will land. Do not |
| 36 | +sleep-and-pray; poll this field. |
| 37 | + |
| 38 | +## Pre-flight checklist (when `source="game"` keeps timing out) |
| 39 | + |
| 40 | +1. **Is `_mcp_game_helper` actually in the project's autoload list?** |
| 41 | + - Open `Project Settings → Autoload`, or grep `project.godot` for |
| 42 | + `_mcp_game_helper="*res://addons/godot_ai/runtime/game_helper.gd"`. |
| 43 | + - If missing: disable + re-enable the Godot AI plugin in Project Settings → |
| 44 | + Plugins. Re-enabling fires `_ensure_game_helper_autoload()` which writes |
| 45 | + the entry and persists it via `ProjectSettings.save()`. |
| 46 | +2. **Was the game launched via `project_run`?** |
| 47 | + - `editor_screenshot(source="game")` requires `_game_run_active=true` on |
| 48 | + the editor side, which is only set by `project_run`. F5-from-keyboard |
| 49 | + plays the game but `mcp:hello` from that play cycle is *explicitly |
| 50 | + ignored*, and you will time out. |
| 51 | +3. **Is the right session active?** |
| 52 | + - Multi-editor / multi-worktree setups: call `session_activate` (or pass |
| 53 | + `session_id` per call) so the screenshot routes to the editor whose game |
| 54 | + is actually running. |
| 55 | +4. **Did the game subprocess actually boot?** |
| 56 | + - Look at the Godot Output panel for |
| 57 | + `[godot_ai game_helper] registered mcp capture (debugger active=true, logger=true)`. |
| 58 | + If that line never prints, the autoload didn't run. If |
| 59 | + `debugger active=false`, you're in a headless / custom-main-loop / |
| 60 | + exported build where the debugger channel is off. |
| 61 | +5. **Did the game crash during boot?** |
| 62 | + - `logs_read` (or `logs_read source="game"`) surfaces any `print`/error |
| 63 | + output the game emitted before dying. A crashed game can never beacon. |
| 64 | + |
| 65 | +## Decision tree for the timeout error |
| 66 | + |
| 67 | +`Game-side autoload never registered its debugger capture within 20s`: |
| 68 | + |
| 69 | +- `is_playing` was **false** when you called `editor_screenshot`? |
| 70 | + → The game wasn't running. Call `project_run` first and poll readiness. |
| 71 | +- `is_playing=true` but `game_capture_ready` stayed **false**? |
| 72 | + → Either the autoload isn't in `project.godot` (item 1 above), or the |
| 73 | + project was launched outside `project_run` (item 2), or the game's |
| 74 | + `_ready` errored before reaching the `mcp:hello` send (check `logs_read |
| 75 | + source="game"`). |
| 76 | +- Worked once, fails on second attempt within the same play cycle? |
| 77 | + → Did you `project_manage(op="stop")` and forget to `project_run` again? |
| 78 | + Each new run rotates a token; the readiness flag is reset on |
| 79 | + `begin_game_run()`. |
| 80 | + |
| 81 | +## Things to prefer over screenshots, when possible |
| 82 | + |
| 83 | +Screenshots are the slowest, flakiest assertion surface — they require |
| 84 | +rendering, encoding, and a live debugger bridge, and any of those can fail |
| 85 | +intermittently. When you can, assert on **state** instead of pixels: |
| 86 | + |
| 87 | +- `node_get_properties` to read the actual visible/visibility/text/etc. of a |
| 88 | + Control after the menu opens. |
| 89 | +- `print()` from the game's `_pressed()` handler — game prints are forwarded |
| 90 | + back over `mcp:log_batch` and surface in `logs_read source="game"`. The AI |
| 91 | + can grep for `"menu_opened"` instead of trying to OCR a screenshot. |
| 92 | +- `node_find` with a query like "find a Control named MainMenu that's |
| 93 | + visible" — gives a yes/no without ever rendering. |
| 94 | + |
| 95 | +Reach for `source="game"` screenshots when the assertion is genuinely |
| 96 | +visual (layout, colors, particle bursts, animation poses) and skip them |
| 97 | +when state inspection would do. |
| 98 | + |
| 99 | +## Reproducing the timeout deterministically |
| 100 | + |
| 101 | +`script/local-game-capture-diag` (developer-facing, runs against your local |
| 102 | +editor) walks through the full bridge end-to-end against the currently-open |
| 103 | +scene and prints diagnostics on failure. Use it when you can't tell whether |
| 104 | +the bug is in your project, in the plugin, or in the AI's calling pattern. |
| 105 | + |
| 106 | +`script/ci-game-capture-smoke` is the CI equivalent — it requires the |
| 107 | +fixture scene `test_project/capture_smoke.tscn` and asserts pixel colors at |
| 108 | +known coordinates. |
0 commit comments