Implement native opening scenes, battle foundation and overworld movement
Checks / web (push) Canceled after 0s
Checks / web (push) Canceled after 0s
This commit is contained in:
1 parent
8b80d05ca2
commit
c08d250cf6
39 files changed
+10727
-1524
No files matched your search
@@ -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.",
|
||||
);
|
||||
Reference in new issue
Block a user