Implement native opening scenes, battle foundation and overworld movement
Checks / web (push) Canceled after 0s

This commit is contained in:
Sébastien QUEROL committed 2026-09-23 16:09:22 +02:00
1 parent 8b80d05ca2
commit c08d250cf6
39 files changed
+10727 -1524

No files matched your search

+195
View File
@@ -51,6 +51,74 @@ const names = {
sprites: "OverworldSprites",
facings: "Facings",
standardScripts: "StdScripts",
mapScenes: "MapScenes",
specials: "SpecialsPointers",
emotes: "Emotes",
landmarks: "Landmarks",
weekdayNames: "SetDayOfWeek.WeekdayStrings",
whatDayText: "SetDayOfWeek.OakTimeWhatDayIsItText",
isItDayText: "SetDayOfWeek.OakTimeIsItText",
dstConfirmText: "InitialSetDSTFlag.DSTIsThatOKText",
timeConfirmText: "InitialClearDSTFlag.TimeAskOkayText",
baseData: "BaseData",
pokemonNames: "PokemonNames",
pokemonPics: "PokemonPicPointers",
picsBanks: "FixPicBank.PicsBanks",
pokemonPalettes: "PokemonPalettes",
growthRates: "GrowthRates",
evosAttacks: "EvosAttacksPointers",
moves: "Moves",
moveNames: "MoveNames",
itemNames: "ItemNames",
itemAttributes: "ItemAttributes",
itemPocketNames: "ItemPocketNames",
giveItemScript: "GiveItemScript",
putItemInPocketText: "PutItemInPocketText",
pocketIsFullText: "PocketIsFullText",
caughtAskNicknameText: "CaughtAskNicknameText",
nicknameStrings: "NamingScreenJumptable.NicknameStrings",
wildBattleScript: "WildBattleScript",
battleWhiteoutScript: "Script_BattleWhiteout",
spawnPoints: "SpawnPoints",
johtoGrassWildMons: "JohtoGrassWildMons",
grassMonProbTable: "GrassMonProbTable",
grassCollisions: "CheckGrassCollision.blocks",
fadeOutToWhite: "FadeOutToWhite",
healParty: "HealParty",
warpToSpawnPoint: "WarpToSpawnPoint",
battleBGMap: "BattleBGMap",
halveMoney: "HalveMoney",
getWhiteoutSpawn: "GetWhiteoutSpawn",
giveItemDummy: "GiveItemScript_DummyFunction",
restartMapMusic: "RestartMapMusic",
setDayOfWeek: "SetDayOfWeek",
initialSetDst: "InitialSetDSTFlag",
initialClearDst: "InitialClearDSTFlag",
toggleDecorationsVisibility: "ToggleDecorationsVisibility",
toggleMaptileDecorations: "ToggleMaptileDecorations",
};
// Work RAM symbols are compared by their CPU address, not a ROM offset.
const ram = new Map(
readFileSync(`${root}/pokecrystal11.sym`, "utf8")
.split("\n")
.flatMap((line) => {
const match = /^0[01]:([c-d][0-9a-f]{3}) (\S+)$/.exec(line);
return match ? [[match[2], parseInt(match[1], 16)]] : [];
}),
);
const ramNames = {
stringBuffer1: "wStringBuffer1",
stringBuffer2: "wStringBuffer2",
stringBuffer3: "wStringBuffer3",
stringBuffer4: "wStringBuffer4",
stringBuffer5: "wStringBuffer5",
playerName: "wPlayerName",
momsName: "wMomsName",
rivalName: "wRivalName",
battleMonNickname: "wBattleMonNickname",
enemyMonNickname: "wEnemyMonNickname",
curPartyLevel: "wCurPartyLevel",
monOrItemNameBuffer: "wMonOrItemNameBuffer",
};
const profile = readFileSync("src/game/rom/profile.ts", "utf8");
for (const [key, symbol] of Object.entries(names)) {
@@ -58,6 +126,133 @@ for (const [key, symbol] of Object.entries(names)) {
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(`${key}:\\s*(0x[0-9a-f]+)`, "i").exec(profile);
if (!match || Number(match[1]) !== ram.get(symbol))
throw new Error(`Profile RAM mismatch: ${key} / ${symbol}`);
}
// BATTLE entries are keyed by their PRET labels.
{
const block = /export const BATTLE = \{([\s\S]*?)\} as const;/.exec(profile)?.[1];
if (!block) throw new Error("Battle symbol table is missing.");
for (const [, quoted, bare, literal] of block.matchAll(/^\s+(?:"([^"]+)"|(\w+)):\s*(0x[0-9a-f]+),/gm)) {
const label = quoted ?? bare;
if (symbols.get(label) !== Number(literal))
throw new Error(`Battle symbol mismatch: ${label}`);
}
}
const eventIds = new Map();
{
let next = 0;
for (const raw of readFileSync(
`${root}/constants/event_flags.asm`,
"utf8",
).split("\n")) {
const line = raw.split(";")[0].trim();
let match;
if (line.startsWith("const_def")) next = 0;
else if ((match = /^const_next\s+(\d+)$/.exec(line)))
next = Number(match[1]);
else if ((match = /^const_skip(?:\s+(\d+))?$/.exec(line)))
next += Number(match[1] ?? 1);
else if ((match = /^const (\w+)$/.exec(line)))
eventIds.set(match[1], next++);
}
}
const eventNames = {
playersRoomPoster: "EVENT_PLAYERS_ROOM_POSTER",
playersHouseConsole: "EVENT_PLAYERS_HOUSE_2F_CONSOLE",
playersHouseDoll1: "EVENT_PLAYERS_HOUSE_2F_DOLL_1",
playersHouseDoll2: "EVENT_PLAYERS_HOUSE_2F_DOLL_2",
playersHouseBigDoll: "EVENT_PLAYERS_HOUSE_2F_BIG_DOLL",
};
for (const [key, name] of Object.entries(eventNames)) {
const match = new RegExp(`${key}:\\s*(\\d+)`).exec(profile);
if (!match || Number(match[1]) !== eventIds.get(name))
throw new Error(`Profile event mismatch: ${key} / ${name}`);
}
// Count constants up to a DEF, the way const_def/const_value do.
const constantCount = (file, name) => {
let value = 0;
for (const raw of readFileSync(`${root}/${file}`, "utf8").split("\n")) {
const line = raw.split(";")[0].trim();
if (line.startsWith("const_def"))
value = Number(/const_def\s+(\d+)/.exec(line)?.[1] ?? 0);
else if (/^const \w+$/.test(line)) value++;
else if (line.startsWith(`DEF ${name} EQU const_value - 1`))
return value - 1;
}
throw new Error(`${name} not found`);
};
for (const [name, file] of [
["NUM_POKEMON", "constants/pokemon_constants.asm"],
["NUM_ITEMS", "constants/item_constants.asm"],
])
if (!profile.includes(`${name} = ${constantCount(file, name)};`))
throw new Error(`${name} does not match the reference.`);
// PRET constants: const_def/const/const_skip/const_next/shift_const and
// numeric or aliased DEF ... EQU, plus the battle command macro list.
const pretConstants = new Map();
{
const number = (text) =>
text.startsWith("$")
? parseInt(text.slice(1), 16)
: text.startsWith("%")
? parseInt(text.slice(1), 2)
: Number(text);
const files = [
...execFileSync("ls", [`${root}/constants`], { encoding: "utf8" })
.split("\n")
.filter((name) => name.endsWith(".asm"))
.map((name) => `${root}/constants/${name}`),
`${root}/macros/scripts/battle_commands.asm`,
];
for (const file of files) {
let value = 0,
step = 1;
for (const raw of readFileSync(file, "utf8").split("\n")) {
const line = raw.split(";")[0].trim();
let match;
if ((match = /^const_def(?:\s+(-?[$%]?[0-9a-fA-F]+))?(?:\s*,\s*(-?\d+))?$/.exec(line))) {
value = match[1] ? number(match[1].replace(/^-/, "")) * (match[1].startsWith("-") ? -1 : 1) : 0;
step = match[2] ? Number(match[2]) : 1;
} else if ((match = /^const_next\s+([$%]?[0-9a-fA-F]+)$/.exec(line))) value = number(match[1]);
else if ((match = /^const_skip(?:\s+(\d+))?$/.exec(line))) value += step * Number(match[1] ?? 1);
else if ((match = /^(const|shift_const|command)\s+(\w+)$/.exec(line))) {
const stored = match[1] === "shift_const" ? 1 << value : value;
pretConstants.set(match[2], stored);
if (match[1] === "shift_const") pretConstants.set(`${match[2]}_F`, value);
value += step;
} else if ((match = /^DEF\s+(\w+)\s+EQU\s+([$%]?[0-9a-fA-F]+|\w+)$/.exec(line))) {
const target = match[2];
const resolved =
target === "const_value"
? value
: /^[$%]?[0-9]/.test(target) || /^[$%]/.test(target)
? number(target)
: pretConstants.get(target);
if (resolved !== undefined) pretConstants.set(match[1], resolved);
}
}
}
}
const battleConstants = readFileSync("src/game/battle/constants.ts", "utf8");
for (const [, block] of battleConstants.matchAll(/export const \w+ = \{([\s\S]*?)\} as const;/g)) {
for (const [, name, literal] of block.matchAll(/^\s+(\w+):\s*(0x[0-9a-f]+|\d+),/gim)) {
const expected = pretConstants.get(name);
if (expected === undefined || ((expected + 256) % 256 !== Number(literal) % 256 && expected !== Number(literal)))
throw new Error(`Battle constant mismatch: ${name} is ${literal}, PRET has ${expected}`);
}
}
const groups = readFileSync(`${root}/data/maps/maps.asm`, "utf8")
.split(/^MapGroup_/m)
.slice(1)
.map((group) => (group.match(/^\tmap /gm) ?? []).length);
if (
!profile.includes(`MAP_GROUP_COUNT = ${groups.length};`) ||
!profile.includes(`LAST_MAP_GROUP_SIZE = ${groups.at(-1)};`)
)
throw new Error("Map group bounds do not match the reference.");
console.log(
"User ROM, pinned PRET rebuild and all native profile addresses match.",
);