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>
6.3 KiB
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/mazegameis themsx-music-generatorrepo emitting lVGM + ayFX, on MSX-Music (YM2413). This project targets plain MSX2, where FM is not standard hardware. Decision deferred to M9; seedocs/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/andemul/rom/kissaten.rom) - Run:
./build.sh run→ builds and launches openMSX - Requires
nodeon 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.tclusingafter time <s> {screenshot f.png}andkeymatrixdown 8 1(SPACE). Addset throttle offto the script: emulated time then runs many times faster than wall clock, so a 200-second play-through takes a couple of seconds.after timestill 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 anout(row select) thenin; 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 = trueis 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:
LibModulesmust include both"tool/disk_save"and"dos". The module alone will not link. Seeprojects/samples/s_save.jsfor the reference configuration, ands_save.cfor usage.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.- 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 offin the test script so this costs seconds of wall clock, not minutes. DiskSave_Check()returnsSAVEDATA_UNSIGNEDfor perfectly good files. WithAppSignature = true(-DAPPSIGN),DiskSave_Check()requires the file's first four bytes to equalg_AppSignature— butDiskSave_Save()writes the payload raw and never adds it. TreatSAVEDATA_UNSIGNEDas success;SaveStatecarries 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.)