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>
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user