Files
jurjen.ladeniusandClaude Opus 5 cbde457da5 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>
2026-07-27 22:17:49 +02:00

6.3 KiB
Raw Permalink Blame History

Kissaten Yūgure — MSX2 cozy game

Project overview

A cozy coffee-shop (kissaten) simulation for MSX2, set in a provincial Japanese town in 1987. Target audience: adult retro gamers who love Japanese art and computers. Written in C using the MSXgl library. Working title: "Kissaten Yūgure" (Twilight Coffee Shop).

Read docs/DESIGN.md before making any design or architecture decisions. Read docs/CONVERSATION.md for the original design discussion and rationale.

Design pillars (never violate)

  • Cozy contract: no fail states, no bankruptcy, no time pressure outside the brew minigame. Progression is always forward.
  • Atmosphere over mechanics: palette work, music, and character writing carry the game.
  • Static-heavy rendering: no scrolling. Only sprites and small blits move.
  • Small, finishable scope. When in doubt, cut.

Tech stack

  • Target: MSX2, SCREEN 5 (G4, 256×212, 16 colors from 512)
  • ROM format: ASCII16 MegaROM (banked) — plan bank layout from day one
  • Library: MSXgl (C) — https://github.com/aoineko-fr/MSXgl
  • Compiler: SDC C via MSXgl build system
  • Music: unresolved — this line is stale. It describes a PSG / MML → NDP pipeline, but the working pipeline used for projects/mazegame is the msx-music-generator repo emitting lVGM + ayFX, on MSX-Music (YM2413). This project targets plain MSX2, where FM is not standard hardware. Decision deferred to M9; see docs/PLAN.md §8 item 15.
  • Dialogue: authored in YAML/JSON, compiled to C arrays / binary banks by tools/compile_dialogue.js (Node.js)
  • Emulator for testing: openMSX (also verify on real hardware profiles)

Build & test

  • Build: ./build.sh (MSXgl build_tool; ROM → out/ and emul/rom/kissaten.rom)
  • Run: ./build.sh run → builds and launches openMSX
  • Requires node on PATH, else build.sh falls back to the bundled Windows/Linux binary and dies: export PATH="/opt/homebrew/opt/node@24/bin:$PATH"
  • Headless verification: drive openMSX with a Tcl script — openmsx -machine C-BIOS_MSX2 -carta emul/rom/kissaten.rom -script test.tcl using after time <s> {screenshot f.png} and keymatrixdown 8 1 (SPACE). Add set throttle off to the script: emulated time then runs many times faster than wall clock, so a 200-second play-through takes a couple of seconds. after time still counts emulated seconds, so timings hold.
  • Boot is slow — budget for it. ROMDelayBoot = true (needed for disk access, see below) means the game only appears at roughly 40-90s of emulated time. Screenshots before that show a blue BASIC screen, which looks exactly like a hang and is not one. Test with a disk machine: openmsx -machine Philips_NMS_8250 -carta emul/rom/kissaten.rom -diska save.dsk
  • Dialogue data (stage 3): node tools/compile_dialogue.js data/dialogue/*.yaml
  • Music data (stage 3+): node tools/generate_mml.js → NDP compiler → data/music/

Platform gotchas (learned the hard way)

  • Keyboard: read the matrix row inside DisableInterrupt()/EnableInterrupt(). Keyboard_Read() is an out (row select) then in; the BIOS ISR scans the matrix too, and if it lands between the two you get phantom keypresses. INPUT_KB_UPDATE (buffered mode) is not a fix — it reads the BIOS NEWKEY/OLDKEY area, which C-BIOS does not maintain in the standard format.
  • BankedCall = true is off until banked code actually exists. With it on and nothing banked, RAM globals were corrupted at runtime (state machine ran wild, counters garbage). Re-enable deliberately at stage 3.
  • 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 — WORKING

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.

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.

Four things are required, none of them obvious:

  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.

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.

Absent or unusable disk is not an error the player has to handle — it just means nothing persists, and the morning card says so.

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.

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.)