mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-27 22:45:34 -04:00
e2454bf568
Second wave of fixes from the Gen 3 codebase review (210 findings), the
regressions the full-suite sweep surfaced, and a T6 tier so the top-level
tests/game3_*.lua suites run inside ./scripts/test.sh instead of outside it.
Battle: Knock Off and Thief/Trick item persistence follow the ROM, not the
review - the party slot is never written through during battle, and the
knocked-off mask suppresses the item on later send-outs
(pokefirered/src/battle_script_commands.c:2731 MOVE_EFFECT_KNOCK_OFF with the
STICKY_HOLD guard at :2732, opponent-steal guard at :2610-2622, mask use at
:4489). Rapid Spin keeps its one-per-use chain order (:8435-8474); Growl
keeps pret's target (src/data/battle_moves.h, include/battle.h:63).
Scripting: operand layouts match pret/asm/macros/event.inc - givemon 15 bytes
(:989-997), comparestat {B,W} (:1573-1576), setptr/loadbytefromptr/setptrbyte
take a word pointer (:118-137). 18 of the 20 E10 ops are wired per
src/scrcmd.c (per-op citations in docs/game3/e10-opcode-spec.md), money and
random and the warp family read their operands as VarGets
(src/scrcmd.c:1798-1830, :455-461, :719-731), and the day-care party-full
guard is in place (src/daycare.c:525,:1081).
Field and UI: fishing counts rounds as the ROM does
(src/field_player_avatar.c:1740-1765), an unresolved map section no longer
reports Pallet Town (src/region_map.c:3782), and a font provider sits behind
a FireRed capability/profile seam.
Save data: profile-driven Options.block, a save version round-trip, and slot
id validation that blocks path traversal (slotDiskPath("firered", "../evil")
returns nil).
Importer: the parallel import path now writes both completion markers,
object kind/clone bytes decode as pret defines them
(include/global.fieldmap.h:110-130), and the HM table drops Whirlpool
(include/constants/items.h:411-418).
Tests: six new scenario suites (battle_ai, capture, event, menu, move,
overworld) and eight engine suites (profile, capabilities, cache paths, font
provider, options block, save version round-trip, version dispatch,
versions_game). T6 runs every top-level game3 suite at GAME3_JOBS default 8
with per-suite logs, failure cause classification and a KNOWN_GAME3_FAILURES
ceiling of 23 as a shrinking guard.
Docs: the working notes under docs/game3 (triage ledger, pret citation audit,
e10 opcode spec, RSE seams, sweep v3/v113, test baseline, artifact
conversions, merge trial).
Full gate at this snapshot: exit 0, all tiers passed - engine 624/624,
T6 279/279 (0 known, 0 fresh failures), gen2 146/146, modkit 37/37, luacheck
clean, privacy gate 3/3 over a 9,711-file publication set.
151 lines
6.9 KiB
Lua
151 lines
6.9 KiB
Lua
-- FireRed profile row: resolution rules + the value contract the wiring
|
|
-- tickets in docs/game3/rse-seams.md depend on.
|
|
--
|
|
-- The profile is deliberately inert until the handoff tickets wire it, so
|
|
-- this suite is the guard that keeps its FireRed row equal to the constants
|
|
-- the engine reads today. When a wiring ticket lands, the assertion it
|
|
-- replaces moves from a literal to a comparison against the live module.
|
|
-- luajit tests/engine/game3_profile_test.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
|
|
local GameVersion = require("src.core.GameVersion")
|
|
local Profile = require("src.core.game3.profile")
|
|
local MapIds = require("src.core.game3.map_ids")
|
|
|
|
local prevVersion = GameVersion.get()
|
|
|
|
-- ---------------------------------------------------------------- resolution
|
|
|
|
Profile.reset()
|
|
GameVersion.set("firered")
|
|
local active = Profile.active()
|
|
eq(active.id, "firered", "active() resolves the active Gen 3 game")
|
|
check(Profile.of("firered") == active, "for(id) and active() share one cached row")
|
|
check(Profile.isGame3Version("firered") == true, "isGame3Version(firered) is true")
|
|
check(Profile.isGame3Version("red") == false, "isGame3Version(red) is false")
|
|
check(Profile.isGame3Version("not-a-game") == false, "isGame3Version(unknown) is false")
|
|
|
|
-- Cached identity: repeated lookups return the same table.
|
|
check(Profile.of("firered") == active, "resolution is cached")
|
|
|
|
-- Unknown game ids fail closed to the active game, not to nil.
|
|
local unknown = Profile.of("not-a-game")
|
|
eq(unknown.id, "firered", "an unknown game id falls back to FireRed")
|
|
|
|
-- A fresh process with no Gen 3 game selected still resolves FireRed, so
|
|
-- shared code can require the module during boot and in headless suites.
|
|
Profile.reset()
|
|
GameVersion.set("red")
|
|
eq(Profile.active().id, "firered", "non-Gen3 process fails closed to FireRed")
|
|
|
|
-- Session-scoped capability lookup reads session.version, not the process.
|
|
local caps = Profile.capabilitiesFor({ version = "firered" })
|
|
check(caps.fameChecker == true, "capabilitiesFor(session) reads the session game")
|
|
check(Profile.has({ version = "firered" }, "vsSeeker") == true, "has() reads a flag")
|
|
check(Profile.has({ version = "firered" }, "contests") == false, "has() is false for absent flags")
|
|
|
|
-- ---------------------------------------------------- FireRed value contract
|
|
|
|
Profile.reset()
|
|
GameVersion.set("firered")
|
|
local row = Profile.active()
|
|
|
|
-- Map identity: the profile describes exactly what MapIds accepts today.
|
|
for _, prefix in ipairs(row.map.prefixes) do
|
|
check(MapIds.isGame3Map(prefix .. "ANYTHING") == true,
|
|
"profile prefix " .. prefix .. " is a Game3 map for MapIds")
|
|
end
|
|
check(MapIds.isGame3Map("NOT_A_MAP") == false, "a foreign id is not a Game3 map")
|
|
eq(row.map.enginePrefix, "FR_", "map id synthesis prefix is FR_")
|
|
eq(row.map.legacyPrefixes[1], "SEVII_", "Sevii is the one legacy namespace")
|
|
eq(row.map.newGameStart.map, MapIds.NEW_GAME_START.map, "new-game start map matches MapIds")
|
|
eq(row.map.newGameStart.x, MapIds.NEW_GAME_START.x, "new-game start x matches MapIds")
|
|
eq(row.map.newGameStart.y, MapIds.NEW_GAME_START.y, "new-game start y matches MapIds")
|
|
eq(row.map.newGameStart.healMap, MapIds.NEW_GAME_START.healMap, "heal map matches MapIds")
|
|
eq(row.map.newGameStart.healX, MapIds.NEW_GAME_START.healX, "heal x matches MapIds")
|
|
eq(row.map.newGameStart.healY, MapIds.NEW_GAME_START.healY, "heal y matches MapIds")
|
|
|
|
eq(row.optionsBlock, "firered", "options block is the FireRed key")
|
|
eq(row.species.num, 412, "species count is FRLG's 412")
|
|
eq(row.species.egg, 412, "egg species is FRLG's 412")
|
|
|
|
eq(row.dexArea.defaultKey, "kanto", "dex area default is kanto")
|
|
eq(row.dexArea.dexMax, 151, "dex area max is 151")
|
|
local okGroups, groups = pcall(require, row.dexArea.mapGroups)
|
|
check(okGroups and type(groups) == "table" and next(groups) ~= nil,
|
|
"dexArea.mapGroups names a loadable map-group module")
|
|
|
|
eq(row.badges.count, 8, "eight badges")
|
|
eq(row.badges.flagBase, 0x820, "badge flag base is 0x820")
|
|
local badgeNames = {
|
|
"BOULDER", "CASCADE", "THUNDER", "RAINBOW",
|
|
"SOUL", "MARSH", "VOLCANO", "EARTH",
|
|
}
|
|
for i, name in ipairs(badgeNames) do
|
|
eq(row.badges.names[i], name, "badge " .. i .. " name is " .. name)
|
|
end
|
|
|
|
eq(row.trainers.rivalIds.squirtle, 326, "rival squirtle id")
|
|
eq(row.trainers.rivalIds.bulbasaur, 327, "rival bulbasaur id")
|
|
eq(row.trainers.rivalIds.charmander, 328, "rival charmander id")
|
|
eq(row.trainers.fallback.class, 81, "rival fallback class")
|
|
eq(row.trainers.fallback.pic, 106, "rival fallback pic")
|
|
eq(row.trainers.fallback.name, "TERRY", "rival fallback name")
|
|
eq(row.trainers.music.encounter.girl, 284, "encounter music girl")
|
|
eq(row.trainers.music.encounter.rocket, 283, "encounter music rocket")
|
|
eq(row.trainers.music.encounter.boy, 285, "encounter music boy")
|
|
eq(row.trainers.music.battle.gym, 296, "gym battle music")
|
|
eq(row.trainers.music.victory.gym, 312, "gym victory music")
|
|
eq(row.trainers.music.victory.trainer, 310, "trainer victory music")
|
|
|
|
eq(row.regionMap.switchFlag, "FLAG_SYS_SEVII_MAP_123", "region-map switch flag")
|
|
|
|
-- FRLG-only features are on for FireRed, off by default for anything else.
|
|
for _, flag in ipairs({ "fameChecker", "teachyTV", "vsSeeker", "trainerTower", "seagallop" }) do
|
|
check(row.capabilities[flag] == true, "FireRed capability " .. flag .. " is on")
|
|
end
|
|
|
|
-- The native module list the registry merges today (natives.lua:803-820).
|
|
local expectedNatives = {
|
|
"natives_corner", "natives_cutscene", "natives_daycare", "natives_elevator",
|
|
"natives_events", "natives_fame", "natives_fan_club", "natives_gift",
|
|
"natives_link", "natives_listmenu", "natives_moveteach", "natives_queries",
|
|
"natives_seagallop", "natives_size_record", "natives_tower", "natives_trade",
|
|
}
|
|
eq(#row.nativeModules, #expectedNatives, "native module count matches the registry")
|
|
for i, name in ipairs(expectedNatives) do
|
|
eq(row.nativeModules[i], name, "native module " .. i .. " is " .. name)
|
|
end
|
|
|
|
-- ------------------------------------------------------------ static guards
|
|
|
|
-- FireRed must stay the fallback row: a missing profiles/firered.lua is a
|
|
-- hard error, not a silent nil.
|
|
Profile.reset()
|
|
local okMissing = pcall(function()
|
|
local warn = Profile.of("not-a-game")
|
|
return warn
|
|
end)
|
|
check(okMissing == true, "unknown ids resolve without raising")
|
|
|
|
-- The profile names the modules the wiring tickets will create; keep them
|
|
-- syntactically valid package names so a typo fails here, not at boot.
|
|
for _, rel in ipairs({
|
|
row.font.module,
|
|
row.dexArea.mapGroups,
|
|
row.saveRules,
|
|
}) do
|
|
check(type(rel) == "string" and rel:match("^[%w_%.]+$") ~= nil,
|
|
"module path is well-formed: " .. tostring(rel))
|
|
end
|
|
check(type(row.font.widths) == "string" and #row.font.widths > 0, "font widths path is set")
|
|
check(type(row.font.smallWidths) == "string" and #row.font.smallWidths > 0, "small font path is set")
|
|
check(type(row.extractors) == "table" and #row.extractors > 0, "aux extractor list is set")
|
|
|
|
GameVersion.set(prevVersion)
|
|
T.finish("game3_profile_test")
|