Menu logic + basic save: state dump to file, read back #8

Merged
merith-tk merged 7 commits from muse-lantern/da-game:menu-save-v1 into main 2026-10-23 03:05:22 +00:00
Member

What

The menu now has its save logic, in the basic form Zach specified (2026-10-22): state dump to file, read state from file — refinement discussion later.

SaveGame (src/core/save_game.gd)

New working file boundary alongside the existing #28 snapshot skeleton:

  • save_state(path, state) — writes a state Dictionary as human-readable JSON ({"format": 1, "state": {...}})
  • load_state(path) — reads it back; returns {} and emits load_failed on missing/corrupt/version-mismatched files
  • has_state(path)
  • Backup-first: overwriting an existing save first copies it to <path>.bak — a bad write never takes the last good state with it

The #28 full-snapshot design (item tree, inert defs, migrations) stays in the file as the documented refinement of what goes inside the dict; these functions are the boundary it will use.

Game wiring (src/core/game.gd)

  • One run slot: user://saves/run.json. State = level id + rig origin transform + timestamp
  • New Game writes the slot; returning to the menu from a game re-dumps it while the rig is still where the player stopped, so Continue resumes from exactly there
  • Continue reads it back — reloads the level if the save names a different one, restores the rig wholesale via the new PlayerRig.place_at_transform() (place_at's transform twin; both write the origin, so poses round-trip exactly). Loading replaces state, never merges
  • Continue button now lights up whenever a save exists (checked at boot and on every return to menu) — the panel plumbing for this existed since PR #2
  • Sandbox never reads or writes the save — throwaway state, per the standing rule

Validation (Godot 4.6.3 standard, headless)

New tests/save_check.gd — SAVE CHECK PASS (19 assertions): unit round-trip incl. backup contents, Continue-with-no-save is a no-op, pose round-trips through the real Game flow (position + facing), sandbox byte-isolation of the save file.

Regressions: smoke_menu PASS · arms_check PASS · addon_check PASS · bridge e2e PASS.

Independent of PR #7 (Terrain3D) — disjoint files, merges cleanly in either order.

— Lantern

## What The menu now has its save logic, in the basic form Zach specified (2026-10-22): **state dump to file, read state from file** — refinement discussion later. ### SaveGame (`src/core/save_game.gd`) New working file boundary alongside the existing #28 snapshot skeleton: - `save_state(path, state)` — writes a state Dictionary as human-readable JSON (`{"format": 1, "state": {...}}`) - `load_state(path)` — reads it back; returns `{}` and emits `load_failed` on missing/corrupt/version-mismatched files - `has_state(path)` - **Backup-first**: overwriting an existing save first copies it to `<path>.bak` — a bad write never takes the last good state with it The #28 full-snapshot design (item tree, inert defs, migrations) stays in the file as the documented refinement of what goes *inside* the dict; these functions are the boundary it will use. ### Game wiring (`src/core/game.gd`) - One run slot: `user://saves/run.json`. State = level id + rig origin transform + timestamp - **New Game** writes the slot; **returning to the menu from a game** re-dumps it while the rig is still where the player stopped, so Continue resumes from exactly there - **Continue** reads it back — reloads the level if the save names a different one, restores the rig wholesale via the new `PlayerRig.place_at_transform()` (place_at's transform twin; both write the origin, so poses round-trip exactly). Loading replaces state, never merges - Continue button now lights up whenever a save exists (checked at boot and on every return to menu) — the panel plumbing for this existed since PR #2 - **Sandbox never reads or writes the save** — throwaway state, per the standing rule ## Validation (Godot 4.6.3 standard, headless) New `tests/save_check.gd` — **SAVE CHECK PASS** (19 assertions): unit round-trip incl. backup contents, Continue-with-no-save is a no-op, pose round-trips through the real Game flow (position + facing), sandbox byte-isolation of the save file. Regressions: `smoke_menu` PASS · `arms_check` PASS · `addon_check` PASS · bridge e2e PASS. Independent of PR #7 (Terrain3D) — disjoint files, merges cleanly in either order. — Lantern
SaveGame gains the working file boundary the design always
planned: save_state/load_state/has_state — a state Dictionary as
human-readable JSON, backup-first (an overwrite copies the old
file to .bak before writing). The #28 full-snapshot skeleton stays
as the documented refinement of what goes inside the dict.

Game wiring:
- New Game writes the run slot (user://saves/run.json)
- Returning to the menu from a game re-dumps, rig still where the
  player stopped — Continue resumes from exactly there
- Continue reads the slot back: level (re)load if it differs, rig
  transform restored wholesale via PlayerRig.place_at_transform
- Continue button lights up whenever a save exists (boot + every
  return to menu refresh it)
- Sandbox never reads or writes the save (throwaway by design)

tests/save_check.gd: unit round-trip + backup contents, boot
no-op Continue, dump/restore pose round-trip through the real
Game flow, sandbox byte-isolation. SAVE CHECK PASS; smoke, arms,
addon suites and the bridge e2e still PASS.
CI: Forgejo Actions test workflow (push + PR, status only)
Some checks failed
tests / test (pull_request) Failing after 14s
1aa6b8be12
Runs everything automatable on the pinned Godot 4.6.3 standard
build, downloaded per run:
- project import
- the headless suites (smoke_menu, arms_check, addon_check,
  save_check; terrain_check joins automatically once it lands —
  branches without it skip it in the open). The suites exit 0
  either way, so the step greps their PASS/FAIL output
- the debug bridge e2e, under Xvfb, which exits non-zero itself

No artifacts are uploaded, nothing is published, no release job —
the workflow only reports test status.

Verified locally: YAML parses; the suite step's script runs all
four suites green and exits 0; a deliberate FAIL probe makes it
exit 1.
Author
Member

CI added to this PR (Zach's direction 2026-10-22): .forgejo/workflows/tests.yml — Forgejo Actions that run the automatable tests on push and pull request, status only.

  • Downloads the pinned Godot 4.6.3 standard build per run, imports the project
  • Runs the headless suites: smoke_menu, arms_check, addon_check, save_check. The suites print PASS/FAIL but exit 0 either way, so the step greps their output (a deliberate-FAIL probe locally confirmed it exits 1 when a suite fails)
  • Runs the debug bridge e2e under Xvfb (it exits non-zero on failure itself)
  • terrain_check joins automatically once PR #7 lands it — branches without the file skip it with a log line, not silence
  • No artifacts, no publishing, no release job — the workflow only reports test status, per the brief

Verified locally before pushing: YAML parses, the suite step runs all four suites green, failure path goes red. The first real run is the push of the CI commit itself; its result on Zach's runner is the infra confirmation (xvfb/curl availability on the runner is the one thing I can't check from here).

— Lantern

**CI added to this PR** (Zach's direction 2026-10-22): `.forgejo/workflows/tests.yml` — Forgejo Actions that run the automatable tests on **push and pull request**, status only. - Downloads the pinned **Godot 4.6.3 standard** build per run, imports the project - Runs the headless suites: `smoke_menu`, `arms_check`, `addon_check`, `save_check`. The suites print PASS/FAIL but exit 0 either way, so the step greps their output (a deliberate-FAIL probe locally confirmed it exits 1 when a suite fails) - Runs the debug bridge e2e under Xvfb (it exits non-zero on failure itself) - `terrain_check` joins automatically once PR #7 lands it — branches without the file skip it with a log line, not silence - **No artifacts, no publishing, no release job** — the workflow only reports test status, per the brief Verified locally before pushing: YAML parses, the suite step runs all four suites green, failure path goes red. The first real run is the push of the CI commit itself; its result on Zach's runner is the infra confirmation (xvfb/curl availability on the runner is the one thing I can't check from here). — Lantern
Placement negates velocity; strafe on; harness-proof the suites
Some checks failed
tests / test (pull_request) Failing after 19s
fb3998af7d
Zach's bug (found testing main): walk off the sandbox, fall, return
to the menu, New Game — the body's stored fall velocity punched the
player through the floor. Rule (game-planning #28): placement means
at rest, and velocity is never saved. PlayerRig.place_at and
place_at_transform now zero the body's velocity and ground control
via _reset_motion().

Also: MovementDirect's strafe flag was never set (library default
off = stick X discarded = forward/back only). Strafing is the
decided scheme (game-planning #19); strafe = true in the rig scene.

Test fallout, all harness-only (real boots verified clean): in
--script mode a script keeps its first compile, which runs before
autoload identifiers register — the rig's typed children stay Nil
in any suite loading the game scene, and a mid-body script error
once hung save_check by re-running its body on dirty state.
- save_check: completion latch (abort reports FAIL, never loops);
  strafe read from scene state; velocity assertions moved out
- tests/rig_motion_check.gd (new): clean-process unit check that
  loads rig+body scripts after the broken window and exercises the
  real place_at_transform — velocity and ground control zeroed
- CI suite list gains rig_motion_check

Verified: save_check PASS, rig_motion_check PASS, smoke/arms/addon
PASS, bridge e2e PASS.
Author
Member

Second batch on this PR (fb3998a) — the bugs Zach found testing main tonight:

  • Placement negates velocity (his sandbox-void bug): PlayerRig.place_at / place_at_transform now zero the player body's velocity + ground control. Placement means at rest; velocity is never saved (game-planning #28).
  • Strafing on: the rig's MovementDirect never had its strafe flag set — library default off discarded the stick's X axis entirely (forward/back only). Now strafe = true, per the decided scheme (game-planning #19).
  • Suites hardened (harness-only fallout; real boots verified clean): in --script mode a script keeps its first compile — which runs before autoload identifiers register — so rig typed children stay Nil in scene-loading suites, and a mid-body error once hung save_check by re-running its body on dirty state. save_check now latches (abort = FAIL, never a loop) and reads strafe from scene state; the velocity rule has a dedicated clean-process unit check, tests/rig_motion_check.gd (also added to the CI suite list).

Verified: save_check PASS · rig_motion_check PASS · smoke/arms/addon PASS · bridge e2e PASS.

— Lantern

**Second batch on this PR** (`fb3998a`) — the bugs Zach found testing main tonight: - **Placement negates velocity** (his sandbox-void bug): `PlayerRig.place_at` / `place_at_transform` now zero the player body's velocity + ground control. Placement means at rest; velocity is never saved (game-planning #28). - **Strafing on**: the rig's MovementDirect never had its `strafe` flag set — library default off discarded the stick's X axis entirely (forward/back only). Now `strafe = true`, per the decided scheme (game-planning #19). - **Suites hardened** (harness-only fallout; real boots verified clean): in `--script` mode a script keeps its first compile — which runs before autoload identifiers register — so rig typed children stay Nil in scene-loading suites, and a mid-body error once hung save_check by re-running its body on dirty state. save_check now latches (abort = FAIL, never a loop) and reads strafe from scene state; the velocity rule has a dedicated clean-process unit check, `tests/rig_motion_check.gd` (also added to the CI suite list). Verified: save_check PASS · rig_motion_check PASS · smoke/arms/addon PASS · bridge e2e PASS. — Lantern
CI: install xvfb before the bridge e2e
Some checks failed
tests / test (pull_request) Failing after 17s
7bc371cf7a
The first approved runner execution (run for the previous head)
got through checkout, the Godot download, import and the GDScript
suites, then died in the e2e step: xvfb-run is not in the runner
image. Install xvfb (and python3, for the same step) when missing,
using sudo only when not already root.
CI: fetch xvfb from the Debian archive (bullseye is EOL)
Some checks failed
tests / test (pull_request) Failing after 33s
61516cc6e2
Run 2984's log: every GDScript suite passed; the xvfb install
step failed — the runner image is Debian bullseye and its default
mirrors have pruned the xvfb dependency versions (404 on every
.deb). Add archive.debian.org sources for bullseye +
bullseye-security and ignore the frozen Release validity dates.
CI: dump the e2e game log on failure; add mesa DRI drivers
Some checks failed
tests / test (pull_request) Failing after 35s
61b0873bcd
Runs 2985-2987: xvfb installs fine from the archive and every
GDScript suite passes, but the bridge e2e's game process exits
with code 1 during startup — and its log (/tmp/da_game_e2e.log)
dies with the runner. Print it when the e2e fails, and install
libgl1-mesa-dri/glx alongside xvfb (the container has no GL stack
of its own; Godot's GLES3 renderer needs one even under Xvfb).
CI: install the X11 client libraries Godot's display server needs
All checks were successful
tests / test (pull_request) Successful in 46s
c697e8be95
Run 2990's dumped game log gives the actual root cause of the e2e
startup exit (not GL): 'libXcursor.so.1: cannot open shared object
file' -> the X11 display server fails, the Wayland fallback fails
with it, Godot exits 1 before the bridge ever starts. xvfb does
not pull the X client libraries in. Add libxcursor1 plus the rest
of Godot's usual X11 runtime set (xinerama, xrandr, xi, xkbcommon).
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
dead-air/engine!8
No description provided.