Read the English 1.0 and Australian cartridges besides 1.1 (#84)
PRET's crystal and crystal_au builds reproduce both retail cartridges, and every label the profile pins keeps its address in all three; check:reference now verifies that when the rebuilds are present. The revision comes from the header. Revision 1.0's Battle Tower draws only the first 21 trainers and picks its texts' gender from the name's sixth character, checked against each cartridge. A native backup imports across the three, which share every map, event and object. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
acdaf62eaa
commit
3c480a28f0
17 files changed
+317
-26
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
const root = ".reference/pokecrystal";
|
||||
@@ -208,13 +208,17 @@ const ramNames = {
|
||||
moneyTemp: "hMoneyTemp",
|
||||
};
|
||||
const profile = readFileSync("src/game/rom/profile.ts", "utf8");
|
||||
// Every label the profile pins, for the other revisions' check below.
|
||||
const used = new Set(["Trainers", "TrainerGroups"]);
|
||||
for (const [key, symbol] of Object.entries(names)) {
|
||||
const match = new RegExp(`\\b${key}:\\s*(0x[0-9a-f]+)`, "i").exec(profile);
|
||||
used.add(symbol);
|
||||
if (!match || Number(match[1]) !== symbols.get(symbol))
|
||||
throw new Error(`Profile address mismatch: ${key} / ${symbol}`);
|
||||
}
|
||||
for (const [key, symbol] of Object.entries(ramNames)) {
|
||||
const match = new RegExp(`\\b${key}:\\s*(0x[0-9a-f]+)`, "i").exec(profile);
|
||||
used.add(symbol);
|
||||
if (!match || Number(match[1]) !== ram.get(symbol))
|
||||
throw new Error(`Profile RAM mismatch: ${key} / ${symbol}`);
|
||||
}
|
||||
@@ -224,6 +228,7 @@ for (const table of ["SAVED_WRAM", "WRAM", "SAVE_BLOCK"]) {
|
||||
if (block === undefined) throw new Error(`${table} symbol table is missing.`);
|
||||
for (const [, quoted, bare, literal] of block.matchAll(/^\s+(?:"([^"]+)"|(\w+)):\s*(0x[0-9a-f]+),/gm)) {
|
||||
const label = quoted ?? bare;
|
||||
used.add(label);
|
||||
if (ram.get(label) !== Number(literal))
|
||||
throw new Error(`${table} RAM mismatch: ${label}`);
|
||||
}
|
||||
@@ -243,9 +248,11 @@ for (const table of ["SAVED_WRAM", "WRAM", "SAVE_BLOCK"]) {
|
||||
);
|
||||
const block = /export const SRAM = \{([\s\S]*?)\} as const;/.exec(profile)?.[1];
|
||||
if (block === undefined) throw new Error("SRAM symbol table is missing.");
|
||||
for (const [, label, literal] of block.matchAll(/^\s+(\w+):\s*(0x[0-9a-f]+),/gm))
|
||||
for (const [, label, literal] of block.matchAll(/^\s+(\w+):\s*(0x[0-9a-f]+),/gm)) {
|
||||
used.add(label);
|
||||
if (sram.get(label) !== Number(literal))
|
||||
throw new Error(`SRAM mismatch: ${label}`);
|
||||
}
|
||||
}
|
||||
// BATTLE, OVERWORLD, PC, INTRO, STATS, MENU, POKEGEAR, MOM, TRAINER_CARD, POKEDEX, OPTIONS, JOHTO, KANTO and AUDIO entries are keyed by their PRET labels.
|
||||
for (const table of ["BATTLE", "OVERWORLD", "PC", "INTRO", "STATS", "MENU", "POKEGEAR", "MOM", "TRAINER_CARD", "POKEDEX", "OPTIONS", "JOHTO", "AUDIO", "PHONE", "RADIO", "MAIL", "EGGS", "KANTO"]) {
|
||||
@@ -253,6 +260,7 @@ for (const table of ["BATTLE", "OVERWORLD", "PC", "INTRO", "STATS", "MENU", "POK
|
||||
if (block === undefined) throw new Error(`${table} symbol table is missing.`);
|
||||
for (const [, quoted, bare, literal] of block.matchAll(/^\s+(?:"([^"]+)"|(\w+)):\s*(0x[0-9a-f]+),/gm)) {
|
||||
const label = quoted ?? bare;
|
||||
used.add(label);
|
||||
if (symbols.get(label) !== Number(literal))
|
||||
throw new Error(`${table} symbol mismatch: ${label}`);
|
||||
}
|
||||
@@ -439,6 +447,33 @@ if (
|
||||
!profile.includes(`LAST_MAP_GROUP_SIZE = ${groups.at(-1)};`)
|
||||
)
|
||||
throw new Error("Map group bounds do not match the reference.");
|
||||
// The other English cartridges (src/rom/validate.ts) read through the same
|
||||
// profile: when their PRET rebuilds are present (`make crystal crystal_au`),
|
||||
// they must match the retail hashes and keep every pinned label in place.
|
||||
const banked = (file) =>
|
||||
new Map(
|
||||
readFileSync(file, "utf8")
|
||||
.split("\n")
|
||||
.flatMap((line) => {
|
||||
const match = /^([0-9a-f]{2}:[0-9a-f]{4}) (\S+)$/.exec(line);
|
||||
return match ? [[match[2], match[1]]] : [];
|
||||
}),
|
||||
);
|
||||
const pinned = banked(`${root}/pokecrystal11.sym`);
|
||||
const revisions = [];
|
||||
for (const [name, rom, sha1] of [
|
||||
["US/Europe 1.0", "pokecrystal", "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133"],
|
||||
["Australia", "pokecrystal_au", "a0fc810f1d4e124434f7be2c989ab5b5892ddf36"],
|
||||
]) {
|
||||
if (!existsSync(`${root}/${rom}.gbc`) || !existsSync(`${root}/${rom}.sym`)) continue;
|
||||
if (createHash("sha1").update(readFileSync(`${root}/${rom}.gbc`)).digest("hex") !== sha1)
|
||||
throw new Error(`The ${name} rebuild does not match the retail cartridge.`);
|
||||
const other = banked(`${root}/${rom}.sym`);
|
||||
const moved = [...used].filter((label) => other.get(label) !== pinned.get(label));
|
||||
if (moved.length)
|
||||
throw new Error(`Profile labels at other addresses in ${name}: ${moved.slice(0, 10).join(", ")}`);
|
||||
revisions.push(name);
|
||||
}
|
||||
console.log(
|
||||
"User ROM, pinned PRET rebuild and all native profile addresses match.",
|
||||
`User ROM, pinned PRET rebuild and all native profile addresses match${revisions.length ? `, also in ${revisions.join(" and ")}` : ""}.`,
|
||||
);
|
||||
Reference in new issue
Block a user