diff --git a/port/src/legacycrc.c b/port/src/legacycrc.c new file mode 100644 index 00000000..1e255595 --- /dev/null +++ b/port/src/legacycrc.c @@ -0,0 +1,106 @@ +/* + * port/src/legacycrc.c — D297: one-time migration of pre-D284 save-slot CRCs. + * + * WHY A DELIBERATELY-WRONG PRNG LIVES IN THIS TREE (read before "fixing" it): + * + * v0.2.x shipped with a port-side bug in randomGetNextFrom's dsll32/dsrl32 + * semantics (modelled as 32-bit shifts; real MIPS64 shifts by n+32 over the + * full register). D284 (commit 0de99b4d) fixed it. But fileGenerateCRC + * (src/game/crc.c) runs that PRNG off a LOCAL constant seed, so every slot + * checksum stored on disk is a fingerprint of the PRNG implementation that + * wrote it: saves written by v0.2.x validate under the OLD stream and fail + * the NEW one, and fileValidateSaves wipes failing slots (D295/M-148, D297). + * + * This file carries the pre-D284 transform so the eeprom shim can recognize + * a legacy save (old-CRC match) and re-stamp its two checksum words with the + * current-PRNG values before the game validates. No game files are touched; + * the new CRCs come from the game's own fileGenerateCRC — only the OLD PRNG + * is duplicated here, and it must stay bit-exact-WRONG. Do not "fix" the + * shifts below; do not share helpers with port/src/random.c. + * + * When this can be deleted: once no pre-D284 saves are expected in the wild + * (the v0.3.0 release is the last build that meets them un-migrated). Keep + * it at least until then; the cost is ~60 lines of dead-but-harmless code. + * + * Verification: the legacy stream below was re-derived from + * `git show 0de99b4d^:port/src/random.c` and matches tools_pc/d295_crc.py, + * which is bit-exact against compiled C for both PRNGs (see D295 M-148). + */ + +#include +#include "file.h" /* save_data */ + +extern void fileGenerateCRC(u8 *addressA, u8 *addressB, save_data *retval); + +/* Pre-D284 randomGetNextFrom: verbatim body from 0de99b4d^:port/src/random.c + * with the OLD helper semantics inlined ((x << n) & 0xFFFFFFFF etc.). */ +static u32 geLegacyRandomGetNextFrom(u64 *seed) +{ + u64 a3 = *seed; + u64 a1, a2; + + a2 = (a3 << 0x1f) & 0xFFFFFFFFULL; /* dsll32, pre-D284 semantics */ + a1 = a3 << 0x1f; /* dsll */ + a2 = a2 >> 0x1f; /* dsrl */ + a1 = (a1 >> 0) & 0xFFFFFFFFULL; /* dsrl32, pre-D284 semantics */ + a3 = (a3 << 0xc) & 0xFFFFFFFFULL; /* dsll32, pre-D284 semantics */ + a2 = a2 | a1; + a3 = (a3 >> 0) & 0xFFFFFFFFULL; /* dsrl32, pre-D284 semantics */ + a2 = a2 ^ a3; + a3 = a2 >> 0x14; /* dsrl */ + a3 = a3 & 0xfff; + a3 = a3 ^ a2; + + *seed = a3; + return (u32)a3; +} + +/* Same loop as fileGenerateCRC (src/game/crc.c) stepping the legacy PRNG. + * Keep in lockstep with crc.c if that loop ever changes. */ +static void geLegacyGenerateCRC(u8 *addressA, u8 *addressB, save_data *retval) +{ + u8 *byte; + s32 shift = 0; + s64 polynormal = 0x8F809F473108B3C1; /* same local constant seed as crc.c */ + s32 checksum1 = 0; + s32 checksum2 = 0; + + for (byte = addressA; byte < addressB; byte++, shift += 7) { + polynormal += *byte << (shift & 0xF); + checksum1 ^= geLegacyRandomGetNextFrom(&polynormal); + } + for (byte = addressB - 1; byte >= addressA; byte--, shift += 3) { + polynormal += *byte << (shift & 0xF); + checksum2 ^= geLegacyRandomGetNextFrom(&polynormal); + } + retval->chksum1 = checksum1; + retval->chksum2 = checksum2; +} + +/* Migrate up to five consecutive 96-byte save slots in place. A slot is + * migrated iff it FAILS the current-PRNG check but PASSES the legacy one; + * re-stamped via the game's own fileGenerateCRC. Corrupt slots (fail both) + * are left alone for the game's own wipe path. Returns the count migrated. */ +int geLegacyCrcMaybeMigrateSlots(u8 *slots5) +{ + save_data *slots = (save_data *)slots5; + int migrated = 0; + + for (int i = 0; i < 5; i++) { + save_data calc = {0}; /* only chksum1/chksum2 are read back */ + u8 *a = &slots[i].completion_bitflags; /* slot bytes [8..96) */ + u8 *b = (u8 *)&slots[i + 1]; /* end boundary, never dereferenced */ + + fileGenerateCRC(a, b, &calc); + if (calc.chksum1 == slots[i].chksum1 && calc.chksum2 == slots[i].chksum2) + continue; /* current-PRNG save (or already migrated) — idempotent no-op */ + + geLegacyGenerateCRC(a, b, &calc); + if (calc.chksum1 != slots[i].chksum1 || calc.chksum2 != slots[i].chksum2) + continue; /* corrupt: game's fileValidateSaves handles it */ + + fileGenerateCRC(a, b, &slots[i]); /* re-stamp chksum1/chksum2 in place */ + migrated++; + } + return migrated; +} diff --git a/port/src/libultra.c b/port/src/libultra.c index fdb6e9d5..3b48d744 100644 --- a/port/src/libultra.c +++ b/port/src/libultra.c @@ -1225,6 +1225,8 @@ typedef char ge_save_slot_size_check[(sizeof(ge_save_slot) == 96) ? 1 : -1]; * re-checks after the block read. */ extern void fileGenerateCRC(u8 *addressA, u8 *addressB, void *retval); +extern int geLegacyCrcMaybeMigrateSlots(u8 *slots5); /* port/src/legacycrc.c (D297) */ + extern s32 portAllUnlocked; /* port/src/video.c */ /* Bit math mirrors fileGetSaveStageDifficultyTime / fileSetDifficultyStageTime @@ -1337,6 +1339,23 @@ static s32 geEepromRW(u8 block, u8 *buf, int nbytes, int write) geEepromStore(); } else { memcpy(buf, s_eeprom + off, nbytes); + /* D297: one-time migration of pre-D284 slot CRCs (port/src/legacycrc.c). + * This IS the read fileValidateSaves performs (block 4, five slots), so + * the re-stamped checksums are exactly what the game validates — no + * ordering hazard. Runs BEFORE geEepromPatchAllCheats so that only + * pristine+migrated bytes are ever persisted; AllUnlocked patches stay + * read-time-only as before. Block-0 smallSave is deliberately untouched + * (factory seal, see D297). */ + if (block == 4 && nbytes == (int)(sizeof(ge_save_slot) * 5)) { + int migrated = geLegacyCrcMaybeMigrateSlots(buf); + if (migrated) { + memcpy(s_eeprom + off, buf, nbytes); + geEepromStore(); + sysLogPrintf(LOG_INFO, + "eeprom: D297-migrated %d pre-D284 save slot(s) to current CRC", + migrated); + } + } /* fileValidateSaves' block read: five save slots from block 4. */ if (portAllUnlocked && block == 4 && nbytes == (int)(sizeof(ge_save_slot) * 5)) { diff --git a/tools_pc/d295_crc.py b/tools_pc/d295_crc.py new file mode 100644 index 00000000..4c50681e --- /dev/null +++ b/tools_pc/d295_crc.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""D295/M-148: reproduce fileGenerateCRC (src/game/crc.c) with both the pre-D284 +(buggy, shipped in v0.2.x) and post-D284 (HEAD) PRNG semantics, verify against the +user's saved slot CRCs, and emit a patched eep whose slots carry POST-FIX CRCs so +a HEAD build accepts them (proves the difficultytext overflow is still live). + +Save layout in ge007.eep (verified M-148): + bytes 0..31 : smallSave header (block 0) + slot i : file offset 32 + i*96, i = 0..5 (sizeof(save_data)=96, padded) + slot fields : c1(s32) c2(s32) bitflags(1) flag_007(1) music(1) sfx(1) + options(2) cheats(3) pad(1) times[76] +CRC region : &completion_bitflags .. save+1 => slot bytes [8..96), 88 bytes. +""" +import struct, sys + +M64 = (1 << 64) - 1 + +def make_rng(buggy: bool): + def dsll32(x, n): + if buggy: return (x << n) & 0xFFFFFFFF + return (x << (n + 32)) & M64 + def dsrl32(x, n): + if buggy: return (x >> n) & 0xFFFFFFFF + return x >> (n + 32) + def step(seed: int) -> int: + # Mirrors randomGetNextFrom line-by-line. NOTE the plain dsrl/dsll lines + # (a2 >>= 31, a1 = a3 << 31, a2 >>= 20) are NOT the *32 helpers. + a3 = seed & M64 + a2 = dsll32(a3, 0x1F) >> 0x1F # dsll32 then plain dsrl 31 + a1 = dsrl32((a3 << 0x1F) & M64, 0) # plain dsll 31 then dsrl32 0 + a3 = dsrl32(dsll32(a3, 0xC), 0) # dsll32 then dsrl32 0 + a2 = (a2 | a1) ^ a3 + a3 = ((a2 >> 0x14) & 0xFFF) ^ a2 # plain dsrl 20, andi, xor + return a3 & M64 + return step + +def file_generate_crc(data: bytes, buggy: bool): + """Mirror crc.c exactly: shift is NOT reset between the two passes.""" + step = make_rng(buggy) + poly = 0x8F809F473108B3C1 & M64 + c1 = 0; c2 = 0; shift = 0 + for b in data: # forward pass, +7/byte + poly = (poly + ((b << (shift & 0xF)) & M64)) & M64 + shift += 7 + poly = step(poly) # randomGetNextFrom updates the seed IN PLACE + c1 ^= poly & 0xFFFFFFFF + for b in reversed(data): # backward pass, +3/byte, shift continues + poly = (poly + ((b << (shift & 0xF)) & M64)) & M64 + shift += 3 + poly = step(poly) + c2 ^= poly & 0xFFFFFFFF + return c1 & 0xFFFFFFFF, c2 & 0xFFFFFFFF + +def main(): + src = sys.argv[1] if len(sys.argv) > 1 else r"C:\Users\james\Downloads\ge-issue87-artifacts\ge007.eep" + dst = sys.argv[2] if len(sys.argv) > 2 else None + eep = bytearray(open(src, "rb").read()) + assert len(eep) == 2048, len(eep) + + print("slot | stored c1,c2 | oldPRNG c1,c2 | newPRNG c1,c2 | old? new?") + ok_old = ok_new = True + for i in range(5): + off = 32 + i * 96 + sc1, sc2 = struct.unpack_from(" {dst}") + +if __name__ == "__main__": + main()