M2 complete: disk saving works via MSXgl's tool/disk_save module

Replaces the hand-rolled PHYDIO/DSKIO sector writer, which never worked,
with engine/src/tool/disk_save.h — added in MSXgl v1.3.0 for exactly this
("save to disk from a ROM application"). Saves are now a real FAT file,
KISSAT00.SAV, on the disk in drive A.

Four non-obvious requirements, all documented in CLAUDE.md:
- LibModules needs both "tool/disk_save" and "dos"
- ROMDelayBoot = true, or the Disk ROM's INIT never runs
- boot then takes 40-90s of emulated time (use `set throttle off`)
- DiskSave_Check() returns SAVEDATA_UNSIGNED for good files: with APPSIGN
  it wants the first 4 bytes to be g_AppSignature, but DiskSave_Save()
  writes the payload raw and never adds it

Also fixes a save-timing bug: the write now happens in AdvanceDay() after
the day increments, so the file describes the morning the player wakes to.
Saving during the evening made a reload replay that day and double-count
its cups.

Verified: play a full day, cold boot, resume at day 2 with served intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 22:17:49 +02:00
co-authored by Claude Opus 5
parent 45749775fa
commit cbde457da5
5 changed files with 105 additions and 228 deletions
+42 -72
View File
@@ -62,83 +62,53 @@ Read `docs/CONVERSATION.md` for the original design discussion and rationale.
- Erase blits must repaint the *actual* background at that spot — the counter
area has three bands (wainscot / counter top highlight / counter top).
## Disk saving from a cartridge ROM — status: UNRESOLVED
## Disk saving from a cartridge ROM — WORKING
M2 wants saves on a real `.dsk`. The game is a cartridge MegaROM, which makes
this awkward. What is established so far, all verified on
`Philips_NMS_8250` + `-carta` + `-diska`:
Saves go to a real file on a real disk: `KISSAT00.SAV` in the root of the
disk in drive A. The disk stays a normal FAT disk you can inspect and copy.
- **A cartridge ROM does boot fine on a disk machine.** Not a problem.
- **`ROMDelayBoot = true` is required.** A cartridge's INIT normally runs
during the BIOS slot scan, and MSXgl never returns from it — so the Disk
ROM's own INIT never happens, `NMBDRV` stays 0 and the disk hooks are never
patched. This option installs an H.STKE hook and returns to the scan
instead. **Boot becomes much slower** — the game appears at ~40-90s of
emulated time, so early screenshots will show a blue BASIC screen and look
like a hang when they are not.
- **Any call into the disk system must be made from RAM.** Both PHYDIO and
CALSLT reach the Disk ROM at 4000h — the same page as this cartridge — so
the calling instruction is switched out mid-call unless it lives elsewhere.
`Disk_InitStub()` copies a small stub to RAM for exactly this. Calling
directly from ROM black-screens the machine.
- With the above in place, `NMBDRV` reads 2 and `MASTER` is non-zero, so the
Disk ROM *has* initialised by the time the game runs.
**Use `engine/src/tool/disk_save.h`.** MSXgl v1.3.0 added this module
specifically for "save to disk from a ROM application". Do not hand-roll
sector I/O — an earlier attempt using PHYDIO (Main ROM 0144h) and a direct
CALSLT to DSKIO (4010h) had both entry points return carry-clear *while
transferring nothing*, proven with a sentinel that survived the call intact.
**The remaining blocker:** both `PHYDIO` (Main ROM 0144h) and a direct
`CALSLT` to `DSKIO` (Disk ROM 4010h) return **carry clear — "success" — while
never touching the destination buffer.** Proven with a 0xAA sentinel that
survives the call intact. Page 0 is confirmed to be the Main ROM at call time
(0x002D reads 1 = MSX2, 0x0144 holds 0xC3 = `JP`), so the entry points
themselves are addressable.
Four things are required, none of them obvious:
Because a clean error code is *not* proof that a read happened, `Disk_Probe()`
now verifies the buffer actually changed and treats "no change" as no disk.
The game degrades gracefully: no disk means day 1, nothing persists, and the
evening card says so. Nothing crashes on either C-BIOS or a real-BIOS machine.
1. **`LibModules` must include both `"tool/disk_save"` and `"dos"`.** The
module alone will not link. See `projects/samples/s_save.js` for the
reference configuration, and `s_save.c` for usage.
2. **`ROMDelayBoot = true`.** A cartridge's INIT normally runs during the BIOS
slot scan and MSXgl never returns from it, so the Disk ROM's own INIT never
happens and there is no disk system to talk to. This option installs an
H.STKE hook and returns to the scan instead.
3. **Boot becomes slow.** The game appears at roughly 40-90s of *emulated*
time. Screenshots before that show a blue BASIC screen and look exactly
like a hang. Use `set throttle off` in the test script so this costs
seconds of wall clock, not minutes.
4. **`DiskSave_Check()` returns `SAVEDATA_UNSIGNED` for perfectly good
files.** With `AppSignature = true` (`-DAPPSIGN`), `DiskSave_Check()`
requires the file's first four bytes to equal `g_AppSignature` — but
`DiskSave_Save()` writes the payload raw and never adds it. Treat
`SAVEDATA_UNSIGNED` as success; `SaveState` carries its own magic, version
and checksum, which is a stronger check regardless.
Next things to try: check whether the H.PHYD RAM hook is genuinely patched at
that moment; try a different disk machine profile; or reconsider the medium
(see PLAN.md §8 item 1).
**Save timing:** written in `AdvanceDay()` *after* the day counter increments,
so the file always describes the morning the player wakes to. Saving during
the evening instead makes a reload replay the day just finished and
double-count its cups.
**RTC CMOS is not a viable fallback**, despite `RTC_USE_SAVEDATA` being TRUE
in `msxgl_config.h`. Block 3 of the RP-5C01 is 13 nibbles, and MSXgl's
`RTC_SaveData()` (`engine/src/clock.c`) stores exactly **6 bytes** — against a
~40-byte `SaveState`. `RTC_SaveDataSigned()` is tighter still. Remaining
options are cartridge SRAM, the FM-PAC SRAM (MSXgl has a `PAC` module), or
shrinking the save to a 6-byte fingerprint — which would cost per-character
affinity and money, so it is a design decision, not just a storage one.
Reference: MSX2 Technical Handbook ch.5, CLOCK-IC section.
Absent or unusable disk is not an error the player has to handle — it just
means nothing persists, and the morning card says so.
## Architecture conventions
- VRAM page 0: visible shop scene bitmap. Page 1: asset warehouse (sprite
frames, portraits, UI tiles) blitted with VDP commands (HMMM/HMMV).
- Seasons and time-of-day are palette swaps only (32-byte tables); never
duplicate background art per season.
- Sprite mode 2, two layered 16×16 sprites per customer (outline + fill).
- Portraits are 48×48, three expressions per character max.
- Game state lives in a single `SaveState` struct (~40 bytes); see DESIGN.md.
Implemented in `kissaten.c` with a magic word, version byte and checksum;
a failed or absent load is not an error, it just means day one.
- The day is a three-phase machine (morning / open / evening) and the clock is
**event-driven** — customers are the clock. `CUSTOMERS_PER_DAY` is the
single pacing dial.
- Dialogue scenes are data, not code: trigger conditions → portrait,
expression, text, optional choice, effects.
Test:
```
openmsx -machine Philips_NMS_8250 -carta emul/rom/kissaten.rom -diska save.dsk
```
Create a blank 720K disk with `tools/build/msxtar/msxtar -cf save.dsk --dos1 --size=720K`.
## Code style
- C99, MSXgl idioms (u8/u16 types, `msxgl_` module prefixes)
- Keep ISR/VBlank work minimal; game logic in main loop
- Comment bank-switching boundaries explicitly
- Tools are Node.js (plain JS, no framework), living in `tools/`
## Scope ladder (build in this order; every stage must be playable)
1. ✅ Shop scene + one customer + brew minigame + serve loop (no story)
2. ◐ Day/night cycle + save + money — day counter, three phases and the
journal line work; **persistence is blocked** (see the disk section below).
Money deliberately deferred to stage 5, since nothing gates on it before.
3. Dialogue engine + two regulars with short arcs ← real milestone
4. Seasons/weather/palette system
5. Remaining cast, upgrades, endings
`docs/PLAN.md` expands this into ten milestones with per-stage requirements
and a decision queue. `docs/SCRIPT.md` holds the full first-draft script.
**RTC CMOS is not an alternative** for a save this size, despite
`RTC_USE_SAVEDATA` being TRUE in `msxgl_config.h`. Block 3 of the RP-5C01 is
13 nibbles and MSXgl's `RTC_SaveData()` stores exactly **6 bytes**, against a
~40-byte `SaveState`. (MSXgl has no cartridge-SRAM mapper target either; the
`PAC` module's FM-PAC SRAM, 8 x 1024 bytes, is the other real option.)