# Command-Line `.scene` Loader + PLAY-on-Launch Status: proposed — awaiting approval before implementation. ## 1. Goal and boundaries After Phase 5, a tester must be able to launch the standalone Sandbox Stage from the command line with a `.scene` file, have it load that scene, and (optionally) enter PLAY immediately so the scene "just plays" for automated/manual testing. Four hard requirements: 1. A command-line `.scene` loader. 2. A flag to start "Stickman Studio" in PLAY mode. 3. PLAY mode is invalid unless a `.scene` is specified. 4. On scene-load error, show the user a dialog and default to a new (blank) scene. This plan targets `res://scenes/sandbox_stage.tscn` (run standalone), its root script `scripts/sandbox_stage.gd`, a new pure parser, and a new headless test. It does **not** change the stickman editor, `main.tscn`, `project.godot`'s `run/main_scene`, or the `.scene` v1.0 JSON schema. Constraints that shape the design: - There is currently **no** command-line argument parsing anywhere in the repo. - `SandboxStage._ready()` ends with `_update_scene_chrome()` then `_check_autosave_recovery()`. Startup integration must slot in there. - `set_mode(StageMode.PLAY)` (line ~607) writes an autosave via `_save_scene_to_path(AUTOSAVE_PATH, true)` **before** `_enter_play_mode()`. A CLI PLAY transition must not create that autosave. - `_load_scene_from_path(path, is_recovery=false)` surfaces read failures with a **toast**, not a dialog (line ~1284). The CLI path needs a dialog-bearing variant. - `StageSceneIO.read_file(path)` returns `{ok, data, error, warnings}`; `error` is a `String`. - `_show_missing_assets_dialog(paths)` (line ~1383) is the existing `AcceptDialog` pattern to reuse. - The stage starts as a blank Untitled document (`_scene_name = "Untitled"`, `_current_scene_path = ""`, default camera), `current_mode` starts `StageMode.EDIT`. - Tests live in `tests/*.gd`, extend `SceneTree`, instantiate `sandbox_stage.tscn`, and drive internals directly (see `tests/test_phase5_file_features.gd`). No test autoload exists. ## 2. Command-line contract ### 2.1 Flags | Flag | Type | Meaning | | --- | --- | --- | | `--play` | boolean | After a successful `--scene` load, enter `StageMode.PLAY`. | | `--scene ` | value (space-separated) | The `.scene` file to load. | | `--scene=` | value (equals form) | Same, `=` separated. | A **bare positional** token that is not `--`-prefixed is accepted as a convenience scene path, but **only when `--scene` is absent**. Supplying both `--scene` and a positional path is a parse error (avoid silent ambiguity). ### 2.2 Godot separator mechanics Godot consumes its own arguments up to a `--` separator; everything **after** `--` is exposed to the script via `OS.get_cmdline_user_args()`. To also run the sandbox stage instead of the project main scene, the stage scene is passed **positionally before** `--`: ``` Godot --path res://scenes/sandbox_stage.tscn -- ``` `OS.get_cmdline_user_args()` then returns exactly `["--play", "--scene", "/abs/foo.scene"]` (the positional scene path is **not** in the user-args array — it is a Godot argument). ### 2.3 Exact launch commands (macOS binary per project facts) Primary tester flow — open and PLAY: ```bash /Applications/Godot.app/Contents/MacOS/Godot \ --path /Users/ryan/Projects/stickman \ res://scenes/sandbox_stage.tscn \ -- --play --scene /Users/ryan/Projects/stickman/stickmen/test.scene ``` Equivalent forms: ```bash # --scene= form .../Godot --path /Users/ryan/Projects/stickman res://scenes/sandbox_stage.tscn -- --play --scene=res://my_playground.scene # bare positional scene path (no --scene) .../Godot --path /Users/ryan/Projects/stickman res://scenes/sandbox_stage.tscn -- --play /Users/ryan/Projects/stickman/my_playground.scene # load into EDIT (no PLAY) .../Godot --path /Users/ryan/Projects/stickman res://scenes/sandbox_stage.tscn -- --scene res://my_playground.scene ``` Windows equivalent (same tokens, project's established console binary): ```powershell & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --path "C:\Godot4\stickman" res://scenes/sandbox_stage.tscn -- --play --scene "C:\Godot4\stickman\stickmen\test.scene" ``` ### 2.4 Path handling - `res://…`, `user://…`, and absolute paths are passed through unchanged; `StageSceneIO.read_file` / `FileAccess` already handle all three (the stage already stores both `res://` and absolute paths in recents). - The parser performs **no** scheme normalization. A bare relative path (no scheme, not absolute) is resolved by Godot against the process CWD and is therefore fragile; the documented and recommended forms are `res://` (project-relative) or absolute. This keeps the parser pure and testable. ## 3. Files and classes | File | Change | Responsibility | | --- | --- | --- | | `scripts/cli_scene_launcher.gd` | **new** `class_name CliSceneLauncher`, `extends RefCounted` | Pure argument parser; no scene-tree or OS access. Returns a structured result. | | `scripts/sandbox_stage.gd` | modified | Startup integration, the CLI load/error-dialog path, the autosave-suppress guard, and the new error dialog in `_build_ui()`. | | `tests/test_cli_scene.gd` | **new** | Headless parser + startup-integration coverage. | No changes to `StageSceneIO`, `stickman_editor.tscn`, `main.tscn`, `project.godot`, or the `.scene` v1.0 schema. No authored `.tscn` node changes in `sandbox_stage.tscn` (the error dialog is code-built like the other file dialogs). ### 3.1 `CliSceneLauncher` API contract ```gdscript class_name CliSceneLauncher extends RefCounted static func parse(args: Array) -> Dictionary # -> { "scene_path": String, "play": bool, "error": String } # scene_path = "" when no scene requested # play = false unless --play was present # error = "" when valid, else a human-readable message ``` Parsing rules (documented so tests assert them): 1. Scan tokens left to right. 2. `--play` → `play = true`. 3. `--scene` → consume the next token as the scene path; missing/empty value → error. 4. `--scene=` → scene path; empty value → error. 5. A non-`--`-prefixed token → positional scene path; a second positional → error. 6. Both `--scene` and a positional path → error (`"Specify the scene with either --scene or a single positional path."`). 7. Any other `--`-prefixed token → error (`"Unknown option: …"`). 8. After the scan: `play == true` with `scene_path == ""` → error (`"--play requires --scene ."`). The parser enforces requirement 3 (PLAY requires a scene) so the stage can treat any `error` uniformly. ### 3.2 `sandbox_stage.gd` additions ```gdscript var _cli_error_dialog: AcceptDialog = null # code-built in _build_ui() var _cli_startup: bool = false # scoped suppress of autosave-on-Play func _handle_cli_startup(args: Array) -> void # entry point from _ready() func _load_cli_scene(path: String) -> Dictionary # -> { "ok": bool, "error": String } func _show_cli_error_dialog(message: String) -> void ``` `_load_cli_scene` reuses `StageSceneIO.read_file` + `_apply_scene_data(data, path, false)` (the ordinary clean-load path), but returns the read error instead of toasting, so the caller can surface it in a dialog: ```gdscript func _load_cli_scene(path: String) -> Dictionary: var read: Dictionary = STAGE_SCENE_IO.read_file(path) if not bool(read.get("ok", false)): return {"ok": false, "error": String(read.get("error", "Could not load scene."))} if not _apply_scene_data(read["data"], path, false): return {"ok": false, "error": "Could not apply scene data."} return {"ok": true, "error": ""} ``` The existing `_load_scene_from_path` toast path is left untouched for interactive Open. ## 4. Startup flow integration into `_ready()` Replace the final `_check_autosave_recovery()` line in `_ready()` with a single `_handle_cli_startup(OS.get_cmdline_user_args())` call (which internally owns recovery for the no-`--scene` case): ```gdscript # _ready() tail (current -> new) _update_scene_chrome() _check_autosave_recovery() # removed from _ready() body # becomes: _update_scene_chrome() _handle_cli_startup(OS.get_cmdline_user_args()) ``` `_handle_cli_startup(args)` flow: ```text result := CliSceneLauncher.parse(args) if result.error != "": _show_cli_error_dialog(result.error) _check_autosave_recovery() # invalid CLI -> normal blank-session recovery return # stay EDIT / blank if result.scene_path.is_empty(): _check_autosave_recovery() # no --scene -> unchanged behavior return # Explicit --scene: retire autosave and skip recovery (explicit file intent wins). if FileAccess.file_exists(AUTOSAVE_PATH): DirAccess.remove_absolute(AUTOSAVE_PATH) load := _load_cli_scene(result.scene_path) if not load.ok: _show_cli_error_dialog(load.error) return # blank/Untitled stage remains if result.play: _cli_startup = true # suppress the autosave-on-Play write set_mode(StageMode.PLAY) _cli_startup = false ``` ### 4.1 Ordering with autosave recovery (decision) When `--scene` is explicitly given, `_check_autosave_recovery()` is **skipped** and the autosave file is **retired (deleted)** before the CLI load. Justification: - Explicit user intent ("open *this* file, and play it") must win over an offer to recover a previous interrupted session; popping the recovery dialog over a freshly CLI-loaded scene is both wrong and confusing. - `_apply_scene_data(..., is_recovery=false)` already deletes the autosave on success, but the **failure** path never reaches that line; deleting upfront covers both outcomes so a stale autosave cannot prompt on a later launch. - When no `--scene` is given (including the invalid `--play`-only case), recovery is unchanged: `_check_autosave_recovery()` still runs, preserving the existing crash-recovery lifecycle for ordinary launches. ### 4.2 `set_mode` autosave guard ```gdscript # set_mode(), current: if mode == StageMode.PLAY: if not _save_scene_to_path(AUTOSAVE_PATH, true): ... # becomes: if mode == StageMode.PLAY and not _cli_startup: if not _save_scene_to_path(AUTOSAVE_PATH, true): ... ``` `_cli_startup` is a scoped boolean set only around the CLI `set_mode(StageMode.PLAY)` call. This prevents the CLI PLAY transition from writing `user://sandbox_autosave.scene` (which would otherwise be newer than its own source file and trigger a false recovery prompt on the next launch). It does not change the interactive Play button path, and it still routes the transition through the public `set_mode()` so mode bookkeeping (`_sync_mode_buttons`, `_apply_mode_frame`, `_refresh_mode_badge`, `mode_changed`) runs exactly as for a manual Play press. ## 5. Error-dialog and fallback-to-new-scene behavior A new `AcceptDialog` `_cli_error_dialog` is built in `_build_ui()` alongside `_missing_assets_dialog`: ```text SandboxStage/UI └── CliErrorDialog (AcceptDialog) title: "Could Not Load Scene" name: "CliErrorDialog" ``` `_show_cli_error_dialog(message)` sets `dialog_text = message` and calls `popup_centered()`, mirroring `_show_missing_assets_dialog`. It is a **blocking** dialog (not the transient toast), satisfying requirement 4. After the dialog is dismissed, the stage is left exactly as a brand-new document: `Untitled`, empty `World`, default camera, `current_mode == EDIT`. No PLAY is entered. This is the "default to a new (blank) scene" guarantee. The error dialog is also used for the `--play`-without-`--scene` case (requirement 3), with the parser's error text (e.g. `"--play requires --scene ."`). ## 6. `--play` validity rules | CLI input | Behavior | | --- | --- | | `--scene ` | Load the scene, remain EDIT. Title bar shows the loaded scene. | | `--play --scene ` | Load the scene, then `set_mode(StageMode.PLAY)` (autosave suppressed). | | `--play` (no `--scene`) | Parser error → `CliErrorDialog`, remain EDIT/blank. **Never** auto-enter PLAY. | | `--scene ` | `CliErrorDialog` with the read error, remain EDIT/blank. | | unknown flag / two positional paths / `--scene`+positional | Parser error → `CliErrorDialog`, remain EDIT/blank. | ## 7. Dirty / recent state A CLI load uses the **normal clean-load** semantics (`_apply_scene_data(..., is_recovery=false)`): it adopts `_current_scene_path`, clears `_dirty`, adds the path to recents (`_add_recent_scene`), removes the autosave, and updates the title. This is what makes the title bar show `Scene Name — .scene — Stickman Studio` and keeps the Open Recent menu consistent with the launched file. No special "read-only" or "dirty-on-load" CLI state is introduced. ## 8. Verification strategy ### 8.1 Headless test — `tests/test_cli_scene.gd` Run with the project's established headless form (macOS): ```bash /Applications/Godot.app/Contents/MacOS/Godot --headless --path /Users/ryan/Projects/stickman --script res://tests/test_cli_scene.gd ``` Required assertions: 1. **Parser (pure, no scene tree):** `CliSceneLauncher.parse([])` → `{scene_path:"", play:false, error:""}`; `["--play","--scene","res://x.scene"]` → `{scene_path:"res://x.scene", play:true, error:""}`; `["--scene=res://x.scene"]` → scene path set; bare positional `["/abs/x.scene","--play"]` → scene path + play; `["--play"]` → error; `["--scene"]` (missing value) → error; `["--bogus"]` → error; two positionals → error; `--scene` + positional → error. 2. **Load:** instantiate `sandbox_stage.tscn`, write a small valid `.scene` fixture to `user://`, call `_handle_cli_startup(["--scene", fixture])`, assert `_current_scene_path == fixture`, `World` child count matches, `_dirty == false`, `_recent_scenes[0] == fixture`, title contains the scene basename, and the autosave file is absent. 3. **PLAY:** `_handle_cli_startup(["--play","--scene", fixture])` → `current_mode == StageMode.PLAY` and `user://sandbox_autosave.scene` does **not** exist afterward (autosave-on-Play suppressed). 4. **Error → blank:** `_handle_cli_startup(["--scene", "user://definitely_missing_cli.scene"])` → `_cli_error_dialog` visible with non-empty `dialog_text`, `World.get_child_count() == 0`, `current_mode == StageMode.EDIT`. 5. **`--play` without scene:** `_handle_cli_startup(["--play"])` → `_cli_error_dialog` visible, `current_mode == StageMode.EDIT`, never PLAY. 6. **Recovery skipped:** pre-write a valid `user://sandbox_autosave.scene` (autosave metadata), call `_handle_cli_startup(["--scene", fixture])`, assert the autosave file is removed and no `AutosaveRecoveryDialog` was created. Note the repo gotcha (`docs/tech_debt_and_optimizations.md` #22): a newly added `class_name CliSceneLauncher` is not resolvable by `--check-only --script` until an `--editor` pass regenerates `.godot/global_script_class_cache.cfg`. Run once first: ```bash /Applications/Godot.app/Contents/MacOS/Godot --headless --editor --path /Users/ryan/Projects/stickman --quit ``` Then a parse check: ```bash /Applications/Godot.app/Contents/MacOS/Godot --headless --check-only --script res://scripts/cli_scene_launcher.gd --path /Users/ryan/Projects/stickman --quit ``` After the focused suite passes, re-run every existing script under `tests/` using its documented headless command and report suite names/results. ### 8.2 Manual Godot Editor verification Run the exact launch commands from §2.3 in a terminal (not F6) and confirm: - `--scene res://.scene` opens the scene in EDIT with the correct title and Open Recent entry. - `--play --scene res://.scene` opens and immediately plays; the Play mode badge shows `▶️ SIMULATING`; no recovery prompt appears on the following ordinary launch. - `--play` alone shows the "Could Not Load Scene" dialog with `"--play requires --scene ."`, leaves a blank Untitled stage, and does not enter PLAY. - `--scene /nonexistent.scene` shows the dialog with the read error and leaves a blank Untitled stage. - A pre-existing `user://sandbox_autosave.scene` is removed when launching with `--scene` (explicit file intent overrides recovery). ## 9. Approval gate: decisions to accept before implementation The architect recommends approving these as a set: 1. **Flag syntax** — `--play` (boolean) and `--scene ` (with `--scene=` and a single bare-positional convenience), parsed from `OS.get_cmdline_user_args()`; the parser enforces "PLAY requires a scene" as an error. Paths must be `res://`/`user://`/absolute (no scheme guessing). 2. **Autosave policy** — an explicit `--scene` skips `_check_autosave_recovery()` and retires the autosave file; the CLI PLAY transition suppresses the autosave-on-Play write via a scoped `_cli_startup` guard (leaving the interactive Play button untouched). 3. **Failure semantics** — on any CLI parse error or `--scene` load failure, show a blocking `AcceptDialog` (`CliErrorDialog`) and remain on a blank EDIT/Untitled stage; never auto-enter PLAY. A successful CLI load uses normal clean-load semantics (adopts path, clears dirty, updates recents/title). ## 10. Out of scope - Changes to `main.tscn`, `stickman_editor.tscn`, `project.godot` `run/main_scene`, or the `.scene` v1.0 JSON schema. - Reusing the CLI loader to launch the stickman **editor** (`.stk`) — this targets the Sandbox Stage only. - `--headless`-only automation mode, output/logging flags, or a `--quit-after` flag. - Argument path normalization (scheme guessing), environment-variable overrides, or multiple-scene batch loading.