From cbde457da531aa3559663faad43b32abb4a2ae9b Mon Sep 17 00:00:00 2001 From: Jurjen Ladenius Date: Mon, 27 Jul 2026 22:17:49 +0200 Subject: [PATCH] M2 complete: disk saving works via MSXgl's tool/disk_save module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- projects/kissaten_yugure/CLAUDE.md | 114 ++++------ projects/kissaten_yugure/docs/PLAN.md | 14 +- .../kissaten_yugure/emul/rom/kissaten.rom | Bin 131072 -> 131072 bytes projects/kissaten_yugure/kissaten.c | 201 +++++------------- projects/kissaten_yugure/project_config.js | 4 +- 5 files changed, 105 insertions(+), 228 deletions(-) diff --git a/projects/kissaten_yugure/CLAUDE.md b/projects/kissaten_yugure/CLAUDE.md index 0ace5e9..f10ec62 100644 --- a/projects/kissaten_yugure/CLAUDE.md +++ b/projects/kissaten_yugure/CLAUDE.md @@ -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.) diff --git a/projects/kissaten_yugure/docs/PLAN.md b/projects/kissaten_yugure/docs/PLAN.md index d621dc0..de3b178 100644 --- a/projects/kissaten_yugure/docs/PLAN.md +++ b/projects/kissaten_yugure/docs/PLAN.md @@ -76,10 +76,9 @@ rains" patterns. **Produces:** `SaveState` struct, phase state machine, save/load module. -**Status:** day counter, three phases, journal line and `SaveState` are done -and playable. **Persistence is blocked** — sector I/O from a cartridge ROM -reports success without transferring data; full findings in the disk section -of `../CLAUDE.md`. +**Status: ✅ complete.** Day counter, three phases, journal line, `SaveState` +and disk persistence all working. Verified by playing a day, cold-booting, and +resuming at day 2 with the served count intact. **Pacing dial, revisited at the end.** `CUSTOMERS_PER_DAY` in `kissaten.c` is the only number that sets day length. Worth reconsidering at M10 as a @@ -482,7 +481,7 @@ milestone that consumes them. | # | Needed for | Decision | My recommendation | |---|---|---|---| -| 1 | M2 | ~~Save medium~~ → **save disk (.dsk), asset supplied** | ⚠ decided, but **not working yet** — see the disk section in `../CLAUDE.md`. Sector I/O reports success without transferring. **The RTC CMOS fallback I previously suggested is not viable**: block 3 is 13 nibbles, and MSXgl's `RTC_SaveData()` stores **6 bytes**, against a ~40-byte `SaveState`. Real fallbacks are cartridge SRAM, the FM-PAC SRAM, or shrinking the save to a 6-byte fingerprint | +| 1 | M2 | ~~Save medium~~ → **save disk (.dsk)** | ✅ working, via MSXgl's `tool/disk_save` module. Saves `KISSAT00.SAV` to drive A. Four non-obvious requirements — see the disk section in `../CLAUDE.md` | | 2 | M2 | ~~Day length~~ → **5 min / 5 customers** | ✅ done. One constant, `CUSTOMERS_PER_DAY` in `kissaten.c` | | 3 | M2 | ~~Clock model~~ → **event-driven** | ✅ done. Customers are the clock | | 4 | M3 | ~~Who writes the dialogue~~ → **drafted by me, you edit** | ✅ resolved | @@ -510,8 +509,9 @@ project, and both want answering now. `../CLAUDE.md` for everything established so far. 2. ✅ **Aki's beats as prose** — done, along with the other three arcs, in `SCRIPT.md`. Awaiting your edit pass. -3. ⬜ **Unblock saving.** Either keep digging at the disk path, or switch - medium. Note the RTC CMOS fallback is off the table (§8 item 1). +3. ⬜ **M3 vertical slice** — Aki's first six beats, hardcoded, with a scratch + portrait. The next real milestone, and the one that decides whether the + game works (§2, M3). 4. ⬜ **Start the portrait question** (§5) — still the only item with external lead time, and now the longest pole by some distance. diff --git a/projects/kissaten_yugure/emul/rom/kissaten.rom b/projects/kissaten_yugure/emul/rom/kissaten.rom index bab288279572fc47531de57278df24975874ab92..25f674d7bea2e47f27bb002ecb458040714bd3e4 100644 GIT binary patch literal 131072 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 literal 131072 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 zZB7i-AQc+ZG0TDtI{yi3WQUBj_4yqB4}>7{jv)$}&z%_dp)EDxNxN#opRz+D$WDhl z`TL?|q&kWI&k9M36>srMlnnR@?G+FHa}R!)025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO z1egF5U;<2l2`~XBzyz286JP>NfC(@GCcp%k025#WOn?b60Vco%m;e)C0!)AjFaajO K1egF52>mamz#q>5 diff --git a/projects/kissaten_yugure/kissaten.c b/projects/kissaten_yugure/kissaten.c index aff7952..4a6aa00 100644 --- a/projects/kissaten_yugure/kissaten.c +++ b/projects/kissaten_yugure/kissaten.c @@ -7,6 +7,7 @@ // INCLUDES //============================================================================= #include "msxgl.h" +#include "tool/disk_save.h" // Fonts data #include "font/font_mgl_sample6.h" @@ -177,8 +178,7 @@ u8 g_SpacePrev = 0; u8 g_Phase = PHASE_MORNING; u8 g_DayServed = 0; // customers served so far today u8 g_DayQuality = 0; // summed brew quality today, for the journal line -u8 g_DiskErr = 0; // last disk error code (0 = fine) -bool g_HasDisk = FALSE; // a usable save disk was found at boot +bool g_SaveOk = TRUE; // did the last write succeed (shown next morning) //============================================================================= // SAVE STATE (DESIGN.md §6) @@ -213,134 +213,26 @@ typedef struct SaveState g_Save; -// One 512-byte sector staging buffer. Must live in RAM (page 3) — PHYDIO -// writes straight into it, and pages 1-2 are ROM on a cartridge target. -u8 g_Sector[512]; - -// Sector holding the save, derived from the disk's own BPB at boot rather -// than hardcoded, so a 360K disk works as well as the 720K one. -u16 g_SaveSector = 14; - //============================================================================= -// DISK ACCESS +// DISK SAVE //============================================================================= - -// Number of disk drives, maintained by the Disk ROM at boot. Zero means there -// is no disk interface at all — and on such a machine (C-BIOS, for one) the -// PHYDIO hook is not patched and calling it is not safe. -#define M_NMBDRV_ADDR 0xF347 -#define M_MASTER_ADDR 0xF348 // main Disk ROM slot, 0 if there is none - -// PHYDIO trampoline, executed from RAM. // -// PHYDIO (Main-ROM 0144h) reaches the Disk ROM through an inter-slot call, and -// the Disk ROM lives at 4000h — the same page as this cartridge. For the -// duration of the call our ROM is switched out of page 1, so the instruction -// making the call must not itself be in page 1. Copying these twelve bytes to -// RAM and calling *them* keeps the call site alive across the switch. -u8 g_DiskStub[21]; +// Uses MSXgl's tool/disk_save module (added in v1.3.0, "save to disk from a +// ROM application"). It writes a real named file — KISSAT0.SAV — through the +// Disk ROM, so the save disk stays a normal, inspectable FAT disk. +// +// This replaced a hand-rolled PHYDIO/DSKIO sector writer that never worked: +// both entry points returned carry-clear without transferring anything. Do +// not go back to raw sectors; use this module. +// +// Requirements, both non-obvious: +// - LibModules must include BOTH "tool/disk_save" and "dos". +// - ROMDelayBoot = true, or the Disk ROM's INIT never runs (see CLAUDE.md). -void Disk_InitStub() -{ - // Calls DSKIO (Disk ROM 4010h) through CALSLT rather than the Main ROM's - // PHYDIO wrapper at 0144h: PHYDIO jumps to a RAM hook that this machine - // leaves unpatched, so it returns carry-clear — reporting success while - // never touching the buffer. Going straight at the Disk ROM avoids it. - // - // IY high byte must hold the slot ID, and MASTER lives at F348h, so a - // word load from F347h lands it in IYh exactly as CALSLT wants. - static const u8 code[21] = - { - 0xF3, // di - 0xFD, 0x2A, 0x47, 0xF3, // ld iy, (0xF347) ; IYh = MASTER - 0xDD, 0x21, 0x10, 0x40, // ld ix, #0x4010 ; DSKIO - 0xCD, 0x1C, 0x00, // call 0x001C ; CALSLT - 0x38, 0x04, // jr c, err - 0x2E, 0x00, // ld l, #0 ; success - 0xFB, // ei - 0xC9, // ret - 0x6F, // err: ld l, a ; BIOS error code - 0xFB, // ei - 0xC9, // ret - }; - Mem_Copy(code, g_DiskStub, sizeof(code)); -} +// Save slot. Multiple save files would be entries 1, 2, ... on the same disk. +#define SAVE_ENTRY 0 -//----------------------------------------------------------------------------- -// Absolute sector read/write into g_Sector. -// carry clear = read, carry set = write -// A=drive B=sector count C=media ID DE=first sector HL=buffer -// Returns the BIOS error code, or 0 on success. -u8 Disk_Access(u16 sector, u8 write) __NAKED -{ - sector; // on stack - write; - -__asm - push ix - ld ix, #0 - add ix, sp - - ld e, 4(ix) // sector low - ld d, 5(ix) // sector high - ld a, 6(ix) // write flag - - ld hl, #_g_Sector - ld b, #1 // one sector - ld c, #0xF9 // media ID: 720K DS/DD - or a // test flag; also clears carry (= read) - jr z, phydio_go - scf // carry set = write -phydio_go: - ld a, #0 // drive A:. 'ld a,n' preserves flags - call _g_DiskStub // -> PHYDIO, from RAM. Returns code in L - - pop ix - ret -__endasm; -} - -//----------------------------------------------------------------------------- -// Read the BIOS Parameter Block and work out where the data area starts. -// Returns FALSE if no disk responded at all. -bool Disk_Probe() -{ - u8 drives = *(const u8*)M_NMBDRV_ADDR; - - // Both of these are set by the Disk ROM's INIT. Without them the entry - // points below are not safe to call — on C-BIOS this work area is never - // initialised at all, and calling anyway black-screens the machine. - if (drives == 0 || drives > 8) - return FALSE; - if (*(const u8*)M_MASTER_ADDR == 0) - return FALSE; - - Disk_InitStub(); - - // Sentinel: the disk entry points can return carry-clear ("success") - // without having touched the buffer, so a clean error code is not on its - // own proof that a read happened. Verify the buffer actually changed. - Mem_Set(0xAA, g_Sector, 16); - g_DiskErr = Disk_Access(0, 0); - if (g_DiskErr != 0) - return FALSE; - if (g_Sector[0] == 0xAA && g_Sector[11] == 0xAA) - return FALSE; // nothing was read; treat the disk as unusable - - u16 bytesPerSec = g_Sector[11] | ((u16)g_Sector[12] << 8); - u16 reserved = g_Sector[14] | ((u16)g_Sector[15] << 8); - u8 numFATs = g_Sector[16]; - u16 rootEntries = g_Sector[17] | ((u16)g_Sector[18] << 8); - u16 secPerFAT = g_Sector[22] | ((u16)g_Sector[23] << 8); - - if (bytesPerSec != 512) // anything else is not a disk we made - return FALSE; - - // Root directory occupies ceil(entries * 32 / 512) sectors - u16 rootSecs = (u16)((rootEntries + 15) >> 4); - g_SaveSector = reserved + (u16)numFATs * secPerFAT + rootSecs; - return TRUE; -} +bool g_HasDisk = FALSE; // a usable save disk was found at boot //----------------------------------------------------------------------------- u8 Save_Checksum(const SaveState* s) @@ -352,25 +244,42 @@ u8 Save_Checksum(const SaveState* s) return (u8)(0 - sum); } +//----------------------------------------------------------------------------- +// A missing drive or a blank disk is not an error the player has to deal +// with — it just means nothing will persist. +void Disk_Init() +{ + DiskSave_SetName("KISSAT"); // 6 chars max + DiskSave_SetExtension("SAV"); // 3 chars max + g_HasDisk = (DiskSave_Initialize() == SAVEDATA_VALID); +} + //----------------------------------------------------------------------------- // TRUE if a valid save was loaded; FALSE means "start a new game", which is -// also what a blank disk gives us — no error path for the player to hit. +// also what a blank disk gives us. bool Save_Load() { if (!g_HasDisk) return FALSE; - g_DiskErr = Disk_Access(g_SaveSector, 0); - if (g_DiskErr != 0) + // SAVEDATA_UNSIGNED is expected, not an error: with AppSignature on, + // DiskSave_Check() wants the file's first 4 bytes to be g_AppSignature, + // but DiskSave_Save() writes the payload raw and never adds it — so a + // perfectly good file always reports UNSIGNED. Only a genuinely absent or + // unreadable file is a reason to bail; the magic and checksum below are a + // stronger check than the signature would have been anyway. + u8 status = DiskSave_Check(SAVE_ENTRY); + if ((status != SAVEDATA_VALID) && (status != SAVEDATA_UNSIGNED)) return FALSE; - SaveState* s = (SaveState*)g_Sector; - if (s->magic != SAVE_MAGIC || s->version != SAVE_VERSION) - return FALSE; - if (s->checksum != Save_Checksum(s)) + if (!DiskSave_Load(SAVE_ENTRY, (u8*)&g_Save, sizeof(SaveState))) return FALSE; - Mem_Copy(s, &g_Save, sizeof(SaveState)); + // Trust nothing that came off a disk + if (g_Save.magic != SAVE_MAGIC || g_Save.version != SAVE_VERSION) + return FALSE; + if (g_Save.checksum != Save_Checksum(&g_Save)) + return FALSE; return TRUE; } @@ -384,11 +293,7 @@ bool Save_Write() g_Save.version = SAVE_VERSION; g_Save.checksum = Save_Checksum(&g_Save); - Mem_Set(0, g_Sector, sizeof(g_Sector)); - Mem_Copy(&g_Save, g_Sector, sizeof(SaveState)); - - g_DiskErr = Disk_Access(g_SaveSector, 1); - return (g_DiskErr == 0); + return DiskSave_Save(SAVE_ENTRY, (const u8*)&g_Save, sizeof(SaveState)); } //----------------------------------------------------------------------------- @@ -605,6 +510,10 @@ void EnterMorning() Dlg_Clear(); Dlg_Line(0, "Morning. The shutters go"); Dlg_Line(1, "up, the kettle goes on."); + if (!g_HasDisk) + Dlg_Line(2, "(no save disk in drive A)"); + else if (!g_SaveOk) + Dlg_Line(2, "(save failed)"); Dlg_Prompt("[SPACE] open the shop"); } @@ -630,18 +539,17 @@ void EnterEvening() else Dlg_Line(1, "Tomorrow, a steadier hand."); - if (Save_Write()) - Dlg_Prompt("[SPACE] sleep (saved)"); - else if (!g_HasDisk) - Dlg_Prompt("[SPACE] sleep (no disk)"); - else - Dlg_Prompt("[SPACE] sleep (SAVE FAILED)"); + Dlg_Prompt("[SPACE] sleep"); } //----------------------------------------------------------------------------- +// Save *after* the day rolls over, so the file always describes the morning +// the player will wake up to. Saving during the evening instead would make a +// reload replay the day just finished and double-count its cups. void AdvanceDay() { g_Save.day++; + g_SaveOk = Save_Write(); EnterMorning(); } @@ -861,13 +769,10 @@ void main() g_Frame = 0; g_SpaceHit = FALSE; g_SpacePrev = 1; // treat SPACE as held at boot: no phantom first edge - g_DiskErr = 0; - g_SaveSector = 14; - // Save disk. A missing or blank disk is not an error the player has to // deal with — it just means today is day one and nothing will persist. Save_Reset(); - g_HasDisk = Disk_Probe(); + Disk_Init(); Save_Load(); EnterMorning(); diff --git a/projects/kissaten_yugure/project_config.js b/projects/kissaten_yugure/project_config.js index 6cc68d1..53c587a 100644 --- a/projects/kissaten_yugure/project_config.js +++ b/projects/kissaten_yugure/project_config.js @@ -4,7 +4,9 @@ // ASCII-safe name: used for output filenames (ROM, symbols, intermediates) ProjName = "kissaten"; ProjModules = [ ProjName ]; -LibModules = [ "system", "bios", "vdp", "print", "input", "memory", "math" ]; +// "tool/disk_save" needs "dos" alongside it — see projects/samples/s_save.js +LibModules = [ "system", "bios", "vdp", "print", "input", "memory", "math", + "tool/disk_save", "dos" ]; Machine = "2";