15 KiB
P1 gameplay iteration design
This design closes PLAN P1 for Linux and Windows desktop 2D/3D authoring. It improves the path from changing gameplay or a scene to seeing the result in the Player. C++ remains the compiled gameplay language; Lua remains an optional module with development reload. MCP controls the Editor and authoring/build services, not the live Player world.
The Editor's dark visual treatment should be consistent with the iteration console reference and the new-project reference. These images are visual references only. The behavior, data contracts, platform paths, accessibility and available actions in this specification and PLAN.md are the source of truth. The Windows path shown in the project image is illustrative; the real chooser must use each platform's native paths.
Existing baseline and scope
The optional Lua VM, LuaLS annotations, component schemas, Inspector fields, atomic development reload and rollback already exist. BuildService already serializes GUI/MCP builds, uses incremental CMake/Ninja native trees separated by Debug and Release, publishes immutable successful generations, and preserves the last good generation after failure. The authoring service atomically journals every transaction under .faset/recovery and refuses to save over an externally changed scene. A disposable workflow script measures one changed C++ build and first Player frame. These remain the foundation, not new work to replace.
P1 adds verifiable reuse of unchanged schema/build packages, useful compiler diagnostics, convenient new-project choices and code navigation, autosave of named scenes, and repeatable iteration measurements. It also closes the recorded Lua validation gap on Windows and in a graphical Release export. P1 does not add C++ hot reload, dynamic gameplay loading, Lua state preservation across reload, Player-world MCP access, a built-in code editor, or broad performance guarantees. Dynamic gameplay loading is reconsidered only if measured native link time is a material bottleneck.
User workflows
C++ edit, build and Play
The developer edits Scripts/Gameplay.cpp or a header, presses Build, and sees a named job with phase, progress, elapsed time and diagnostics. A compiler error shows severity, a short message, project-relative file and one-based line/column when the tool supplies them. Selecting it opens the source at that location in the configured external editor. Raw compiler output remains available. A failed build leaves the prior Player/schema generation intact and marks Inspector metadata stale. An unchanged second build reports a cache hit and reuses the same validated generation. Play still captures the current authoring scene, invokes the build service, then launches an isolated Player; editing after capture does not silently change that snapshot.
Lua edit and reload
A Lua project declares scripts in project.faset.json and can start with no project C++ files. Script edits mark schema metadata stale when declarations change; Refresh Lua validates schemas without recompiling unrelated C++ gameplay. The running development Player watches or explicitly reloads scripts; successful reload resets its world/script state, while an invalid edit preserves the old running version. The Editor and MCP surface parseable diagnostics with source locations where available. A packaged Release Player includes only the declared Lua snapshot and needs neither slangc, LuaLS nor the Editor.
New project and source navigation
The new-project chooser offers four clear combinations: C++ 2D, C++ 3D, Lua 2D and Lua 3D. The existing CLI --new NAME --dimension 2|3 and two-argument BuildService::scaffold(name, dimension) retain their C++ scaffold behavior for compatibility. The chooser and an explicit --language cpp|lua select a runnable template that adds a minimal start scene and documented starter behavior for the chosen dimension. Template creation never overwrites existing user files and rejects nonempty conflicting destinations. Lua starters include the module declaration and language-server setup without a C++ gameplay stub. The source browser opens C++, headers and Lua under Scripts; diagnostic rows open the exact project source line/column when supported by the configured editor.
faset_source_open accepts a project-relative path and optional one-based line/column, validates that the target is a regular .cpp, .hpp, .h or .lua file beneath Scripts, and launches an argv array without a shell. It rejects traversal, symlinks escaping the project, directories and executable files. The existing faset_script_open Lua-only command remains a compatibility alias. Editor configuration supports {file}, {line}, {column} and {project} tokens. The default Zed invocation uses its path:line:column syntax; custom commands can substitute the tokens. Missing editor executables yield a concise actionable error and never make a build fail.
Autosave
Autosave is enabled by default for named, dirty scene documents while a persistent Editor session is running, including headless MCP sessions. It waits 2 seconds after the last document revision change and saves through AuthoringService::save; rapid edits coalesce. A project-level editor.autosave Boolean in project.faset.json controls it, with a visible toggle in Project settings and a read-only faset_autosave_status command. Changing the toggle takes effect immediately in the current session and persists for the next open. Ctrl+S and faset_document_save remain immediate explicit saves. An unnamed scene is never assigned an implicit path: the UI says Save As required, and the existing recovery journal protects its edits.
Autosave carries the document revision it observed into an optional expected_revision on the save API. A concurrent edit makes that save fail with revision.conflict, after which the newer revision is scheduled normally. Existing disk_hash comparison still rejects external modifications; autosave never silently overwrites them. A failed autosave leaves the document dirty and its journal intact, shows a persistent conflict/error with Save As or reload guidance, and does not log the same failure every frame. Another edit or an explicit retry permits a new attempt. Saving creates no Undo operation; Undo/Redo remain valid after autosave. Scene snapshots already captured for Play/export remain immutable. The recovery journal remains active even when autosave is disabled or before its timer expires.
Build cache and schema correctness
The native build always runs CMake configure and Ninja/selected generator build. Those tools own source/header dependency analysis; a shortcut based only on Session::source_signature() is unsafe because that signature covers project Scripts but not engine sources, shaders, CMake recipes or toolchain changes. A no-op native build should do no compile or link work, but the UI must not call it a native cache hit merely because the later schema/package stage was reused.
After a successful native build, BuildService computes a versioned package key from the selected configuration, normalized build recipe and configure arguments, CMakeCache.txt and toolchain identity, complete project Scripts content snapshot, Lua declaration/fingerprint, Player and SchemaExporter hashes, all required SPIR-V/reflection hashes, and copied runtime-library hashes. The toolchain identity includes resolved compiler/CMake/Slang executable identity or content hash; changing a toolchain in place must force the native tree to be reconfigured/rebuilt or make the service refuse reuse. Asset source files are not gameplay-build inputs.
If the key matches the last successful immutable build, and every required file passes the stored manifest hash and schema validation, the service returns that generation with explicit schema_cache_hit=true and generation_reused=true. It skips SchemaExporter and package copying. A missing, malformed or corrupt cached file is never returned as a hit; the service attempts a fresh candidate or fails while retaining the prior pointer. A changed key runs SchemaExporter against the same captured Lua source snapshot, validates the complete schema, stages all files, rechecks the project source snapshot, then atomically publishes the new generation and pointer. Do not publish a mixed snapshot if any Scripts source changes during the build. Debug and Release have distinct keys and native trees. Export always revalidates referenced assets and packages their current generations even if gameplay build reuse succeeds; a stale asset must block export until reimport.
JobStatus and faset_job expose the reused-generation flag, schema hit flag, elapsed phase times and final build fingerprint. Existing result.directory, result.player, result.schema and the last-good pointer remain compatible. Cache results must be understandable in the GUI and MCP without parsing logs.
Diagnostics and UI behavior
The build service keeps its bounded raw log and adds a structured diagnostics array: {severity, phase, message, file?, line?, column?, code?}. The parser handles Clang/clang-cl and Lua source-location formats on Linux and Windows, including drive letters and Unicode paths. Project files are normalized to project-relative paths; outside-project diagnostics remain visible as text but cannot be passed to faset_source_open. ANSI escape sequences do not contaminate messages. Unrecognized output stays in the raw log; nonzero exit without a parsed error still creates a generic job failure rather than an empty error panel. Multiline notes remain associated with the triggering error where practical.
The Console shows current and recent build jobs, counts by severity, phase, elapsed time, the first actionable error and expandable raw output. Selecting a diagnostic invokes the same faset_source_open command available to MCP. Disabled source actions explain why a location cannot be opened. The status bar distinguishes Saved, Pending autosave, Saving, Save conflict, Save failed and Save As required. Controls remain keyboard reachable, compact, high contrast and consistent with the Editor's dark theme. Layout and copy may evolve from the image reference as the actual controls are implemented.
Measurement and validation
tools/measure_workflows.py retains disposable projects and machine-readable raw results. It records cold configure/build, warm unchanged build, changed C++ source/header build, build failure/recovery, Play-to-first-rendered-frame, Lua edit-to-successful-reload, and repeated Editor event-to-visible-state latency. Run at least five repetitions after a stated warm-up for warm/changed cases; retain each sample plus median and nearest-rank p95. Record exact revision and dirty state, OS, CPU, RAM, GPU/driver, compiler, CMake, Slang, build configuration, project/source hashes, scene size and whether validation/readback were enabled. Keep cold-start samples separate from warm samples; label offscreen first frame and windowed first presented frame separately using the Player's existing startup_ms.main_to_first_frame and presentation_mode profile fields. Include a larger generated scene/content case alongside the checked-in 2D/3D examples. Measurements are observations, not flaky CI timing gates.
For the documented Linux reference host and exact two sample scenes, track the existing budgets: measured frame p95 ≤ 4 ms, GPU and readback p95 ≤ 1 ms each, simulation and snapshot p95 ≤ 0.5 ms each, explicit live Vulkan allocations ≤ 20 MiB, and startup from main() ≤ 500 ms. Run a 3,000-frame warm resource-lifecycle sample to check for growth. Report any missed budget honestly and investigate; do not claim the limits for other machines or content. Record a separate available Windows baseline, distinguishing software Vulkan from physical GPU. If physical Windows hardware is unavailable, mark that coverage unverified rather than blocking the functional Windows CI result.
The Lua module must pass Linux and Windows native CPU suites; the watched/explicit reload integration must run under an available Vulkan device on both CI platforms; and a Lua-only Release export must validate and render after relocation with source project paths unavailable. Both Linux and Windows CI report exact test outcomes. Update the English Manual with C++/Lua iteration recipes, template selection, source editor setup, cache indicators, autosave/conflict recovery, and diagnostic navigation. Update PLAN.md, docs/IMPLEMENTATION.md and a dated validation dossier only after checks and measured evidence exist.
Acceptance matrix
| Area | Required evidence |
|---|---|
| Lua | Existing lifecycle/safety/CLI suites pass on Linux and Windows; watched/explicit reload and relocated Lua-only Release export render on available Vulkan implementations. |
| Cache | Second unchanged build reuses the same verified schema/package generation and does not invoke SchemaExporter; header, Lua, recipe/toolchain, shader or runtime output changes invalidate appropriately; corrupt entries never become hits. |
| Rollback | Failed compile/schema/copy and source-race cases leave the previous successful pointer and Inspector schema available but stale; export still rejects stale assets. |
| Diagnostics | Clang, clang-cl, Lua, Unicode path, drive-letter and unparseable-output fixtures pass; real C++ compile failure produces a navigable diagnostic and raw log. |
| Templates/navigation | All four new-project combinations launch or validate, old CLI/scaffold defaults still work, existing files are not overwritten, source-open rejects traversal and opens project code at the requested position. |
| Autosave | Named scene saves after idle, rapid edits coalesce, unnamed scene remains in recovery, disk/revision conflicts never overwrite, Undo and Play snapshot remain stable; GUI and MCP show status. |
| Iteration | Raw repeated cold/warm/changed/Play/Lua/UI samples and environment metadata are committed; claims are limited to measured scenes/hosts; reference budgets and any misses are shown. |
| Documentation | Strict MkDocs build and compiled tutorial tests pass; Manual, PLAN.md and validation dossier describe observed behavior and known coverage gaps. |
Delivery sequence
Deliver safe cache keys and source snapshots first, then diagnostics/navigation, templates and autosave as independently testable slices. Finish with cross-platform Lua/export validation, repeated measurements, documentation and independent review. Each slice uses a failing contract test before implementation and a focused passing suite before its commit. The final P1 status is only marked complete when the acceptance matrix has linked evidence; unavailable physical Windows GPU or benchmark hardware is recorded as a coverage limit, not silently treated as passed.