Files
mazegame/projects/kissaten_yugure/CLAUDE.md
T
jurjen.ladeniusandClaude Opus 5 45749775fa Scaffold Kissaten Yugure; add M2 day spine, plan and script
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>
2026-07-27 21:58:22 +02:00

8.2 KiB
Raw 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 — 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.