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:
@@ -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.)
|
||||
|
||||
Reference in New Issue
Block a user