Stage 1 (shop scene, customer, siphon brew minigame, serve loop) and the M2 day spine: day counter, three-phase day, event-driven clock, journal line, and a SaveState with magic/version/checksum. Saving to disk is NOT working yet. A cartridge ROM can reach the Disk ROM only from RAM (both PHYDIO and CALSLT switch page 1 out from under the caller), and ROMDelayBoot is required so the Disk ROM's INIT runs at all. With both in place the disk is detected, but sector I/O returns carry-clear without transferring — proven with a sentinel. Probe now verifies the buffer actually changed, so a non-working disk degrades to "no save" rather than corrupting anything. Full findings in the project CLAUDE.md. Also documented: keyboard reads need interrupt protection (the BIOS ISR scans the matrix too), and BankedCall must stay off until banked data exists. Docs: PLAN.md (ten milestones, per-stage requirements, decision queue), SCRIPT.md (all four character arcs, 60 beats, checked against the 26-character box), and amendment notes in DESIGN.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
145 lines
8.2 KiB
Markdown
145 lines
8.2 KiB
Markdown
# 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 — status: UNRESOLVED
|
||
|
||
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`:
|
||
|
||
- **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.
|
||
|
||
**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.
|
||
|
||
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.
|
||
|
||
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).
|
||
|
||
**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.
|
||
|
||
## 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.
|
||
|
||
## 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.
|