M-29: file-backed EEPROM saves (BACKLOG B5 / Phase 4)

port/src/libultra.c: osEeprom{Probe,Read,Write,LongRead,LongWrite} were
no-op stubs, so campaign progress (unlocked missions, times, difficulty)
was lost on every quit. Implement a real 16Kbit (2048-byte) EEPROM backed
by $S/ge007.eep: lazy-load on first access, write-through on every write,
bounds-checked against the 256-block device. Pattern mirrors the PD port
(pd_port/port/src/libultra.c). Probe now reports EEPROM_TYPE_16K so the
decomp save logic (src/game/file2.c, already compiled) runs unchanged.

Verified: build green (ntsc-final); -level_09 boots, the game writes
data/ge007.eep (2048 B, checksum @block 0 + save_data slots @block 4),
600+ frames crash-free; -level_09 unregressed. File persists across runs.
This commit is contained in:
James Dansereau committed 2026-08-30 20:52:47 -04:00
1 parent 609bb732b9
commit d0d18b2472
1 file changed
+59 -7
+59 -7
View File
@@ -980,17 +980,69 @@ s32 osContReset(OSMesgQueue *mq, OSContStatus *status)
return 0;
}
/* EEPROM: file-backed saves land in Phase 4. For now report "no eeprom"
* so the game proceeds without save functionality. */
s32 osEepromProbe(OSMesgQueue *mq) { (void)mq; return -1; }
/* EEPROM: file-backed 16Kbit save (Phase 4, BACKLOG B5). GE addresses the
* device in 8-byte blocks (src/game/file2.c: checksum @block 0, five
* save_data slots from block 4). Backed by $S/ge007.eep, loaded lazily on
* first access and rewritten on every write. Pattern mirrors the PD port
* (pd_port/port/src/libultra.c). */
#define GE_EEP_BLOCKS EEP16K_MAXBLOCKS /* 256 */
#define GE_EEP_SIZE (GE_EEP_BLOCKS * 8) /* 2048 bytes */
#define GE_EEP_PATH "$S/ge007.eep"
static u8 s_eeprom[GE_EEP_SIZE];
static int s_eepromLoaded = 0;
static void geEepromLoad(void)
{
if (s_eepromLoaded) return;
s_eepromLoaded = 1;
const char *path = sysResolvePath(GE_EEP_PATH);
FILE *fp = fopen(path, "rb");
if (fp) {
fread(s_eeprom, 1, GE_EEP_SIZE, fp);
fclose(fp);
sysLogPrintf(LOG_INFO, "eeprom: loaded %s", path);
} else {
memset(s_eeprom, 0, GE_EEP_SIZE);
sysLogPrintf(LOG_INFO, "eeprom: no %s yet (fresh save)", path);
}
}
static void geEepromStore(void)
{
const char *path = sysResolvePath(GE_EEP_PATH);
FILE *fp = fopen(path, "wb");
if (fp) {
fwrite(s_eeprom, 1, GE_EEP_SIZE, fp);
fclose(fp);
} else {
sysLogPrintf(LOG_ERROR, "eeprom: cannot write %s", path);
}
}
static s32 geEepromRW(u8 block, u8 *buf, int nbytes, int write)
{
u32 off = (u32)block * 8;
if (!buf || nbytes < 0 || off + (u32)nbytes > GE_EEP_SIZE) return -1;
geEepromLoad();
if (write) {
memcpy(s_eeprom + off, buf, nbytes);
geEepromStore();
} else {
memcpy(buf, s_eeprom + off, nbytes);
}
return 0;
}
s32 osEepromProbe(OSMesgQueue *mq) { (void)mq; return EEPROM_TYPE_16K; }
s32 osEepromRead(OSMesgQueue *mq, u8 addr, u8 *buf)
{ (void)mq; (void)addr; (void)buf; return 0; }
{ (void)mq; return geEepromRW(addr, buf, 8, 0); }
s32 osEepromWrite(OSMesgQueue *mq, u8 addr, u8 *buf)
{ (void)mq; (void)addr; (void)buf; return 0; }
{ (void)mq; return geEepromRW(addr, buf, 8, 1); }
s32 osEepromLongRead(OSMesgQueue *mq, u8 addr, u8 *buf, int nbytes)
{ (void)mq; (void)addr; (void)buf; (void)nbytes; return 0; }
{ (void)mq; return geEepromRW(addr, buf, nbytes, 0); }
s32 osEepromLongWrite(OSMesgQueue *mq, u8 addr, u8 *buf, int nbytes)
{ (void)mq; (void)addr; (void)buf; (void)nbytes; return 0; }
{ (void)mq; return geEepromRW(addr, buf, nbytes, 1); }
/* Memory Pak (PFS) + Rumble Pak (motor): no accessories on the PC. */
s32 osPfsInit(OSMesgQueue *queue, OSPfs *pfs, int channel)