#!/usr/bin/env luajit -- src/fame_checker.c:119, src/graphics.c:1117, src/mystery_gift_show_card.c:150, -- src/trainer_tower_sets.c:8956, src/data/pokemon/tutor_learnsets.h:22 package.path = "./?.lua;./?/init.lua;" .. package.path local failed = 0 local function check(cond, msg) if cond then print("[ok] " .. msg) else failed = failed + 1 print("[FAIL] " .. msg) end end local function eq(a, b, msg) check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b))) end local function finish() if failed > 0 then print(string.format("[result] %d CHECK(S) FAILED", failed)) os.exit(1) end print("[result] all checks passed") os.exit(0) end local CacheContract = require("src.import.CacheContract") local Versions = require("src.import.gba.versions") local FameCheckerExtract = require("src.import.gba.fame_checker_extract") local TeachyTvExtract = require("src.import.gba.teachy_tv_extract") local MysteryGiftExtract = require("src.import.gba.mystery_gift_extract") local TrainerTowerExtract = require("src.import.gba.trainer_tower_extract") local TutorExtract = require("src.import.gba.tutor_extract") local AltLayouts = require("src.import.gba.alt_layouts") local SHEETS = { { "fame_checker/bg.rgba", 240, 160 }, { "fame_checker/0.rgba", 64, 64 }, { "fame_checker/1.rgba", 64, 64 }, { "fame_checker/13.rgba", 64, 64 }, { "fame_checker/14.rgba", 64, 64 }, { "fame_checker/cursor.rgba", 32, 32 }, { "fame_checker/question_mark.rgba", 16, 32 }, { "teachy_tv/screen.rgba", 240, 160 }, { "teachy_tv/title.rgba", 240, 160 }, { "teachy_tv/end.rgba", 240, 160 }, { "teachy_tv/static.rgba", 32, 8 }, { "teachy_tv/bg3.rgba", 256, 256 }, { "mystery_gift/card_bg0.rgba", 240, 160 }, { "mystery_gift/card_bg3.rgba", 240, 160 }, { "mystery_gift/card_bg7.rgba", 240, 160 }, { "mystery_gift/news_bg0.rgba", 240, 160 }, { "mystery_gift/news_bg7.rgba", 240, 160 }, } print("[test] 1. the ROM offsets are the symbols pret links") eq(Versions.FAME_BG_GFX, 0xE9F260, "FAME_BG_GFX is gFameCheckerBgTiles") eq(Versions.FAME_BG3_TILEMAP, 0xEA0700, "FAME_BG3_TILEMAP is gFameCheckerBg3Tilemap") eq(Versions.FAME_OAK_GFX, 0x45ED80, "FAME_OAK_GFX is sOakSpriteGfx") eq(Versions.FAME_FLAVOR_TEXT_PTRS, 0x45F6BC, "FAME_FLAVOR_TEXT_PTRS is sFameCheckerFlavorTextPointers") eq(Versions.TEACHY_TV_GFX, 0xE86240, "TEACHY_TV_GFX is gTeachyTv_Gfx") eq(Versions.TEACHY_TV_TITLE_TILEMAP, 0xE86D6C, "TEACHY_TV_TITLE_TILEMAP is gTeachyTvTitle_Tilemap") -- src/teachy_tv.c:869 eq(Versions.TEACHY_TV_END_TILES, 0x479590, "TEACHY_TV_END_TILES is sBg1EndGraphic") eq(Versions.WONDER_CARD_GRAPHICS, 0x467FB8, "WONDER_CARD_GRAPHICS is sCardGraphics") eq(Versions.WONDER_NEWS_GRAPHICS, 0x468720, "WONDER_NEWS_GRAPHICS is sNewsGraphics") eq(Versions.TRAINER_TOWER_FLOORS, 0x4827B4, "TRAINER_TOWER_FLOORS is gTrainerTowerFloors") eq(Versions.TT_SINGLES_INFO, 0x479ED8, "TT_SINGLES_INFO is sSingleBattleTrainerInfo") eq(Versions.TUTOR_LEARNSETS, 0x459B7E, "TUTOR_LEARNSETS is sTutorLearnsets") eq(Versions.TUTOR_MOVES + Versions.TUTOR_MOVE_COUNT * 2, Versions.TUTOR_LEARNSETS, "sTutorMoves is 15 u16 and sTutorLearnsets follows it") print("[test] 2. the cache contract requires every new key") local required = CacheContract.requiredFiles("firered") local have = {} for _, path in ipairs(required) do have[path] = true end local WANT = { "data/generated/gba/fame_checker/manifest.lua", "data/generated/gba/fame_checker/bg.rgba", "data/generated/gba/fame_checker/0.rgba", "data/generated/gba/fame_checker/14.rgba", "data/generated/gba/fame_checker/silhouette.pal", "data/generated/gba/fame_checker/pack.lua", "data/generated/gba/teachy_tv/screen.rgba", "data/generated/gba/teachy_tv/title.rgba", "data/generated/gba/teachy_tv/end.rgba", "data/generated/gba/mystery_gift/card_bg0.rgba", "data/generated/gba/mystery_gift/news_bg7.rgba", "data/generated/gba/trainer_tower.lua", "data/generated/gba/pokemon/tutor.lua", "data/generated/gba/native/layouts/alt_366.mid", "data/generated/gba/native/layouts/alt_381.mid", } for _, path in ipairs(WANT) do check(have[path] == true, "the firered contract requires " .. path) end for _, path in ipairs(WANT) do local fs = { prefix = "", exists = function(candidate) return candidate ~= path end, } local complete, missing = CacheContract.allRequiredFilesExist("firered", fs) check(complete == false and missing == path, "a cache without " .. path .. " is incomplete (" .. tostring(missing) .. ")") end -- src/trainer_tower.c:554 SetCurrentMapLayout(sFloorLayouts[floorIdx][challengeType]) local altIds = {} for _, row in ipairs(AltLayouts.TOWER_ROWS) do altIds[row.id] = row end for id = 366, 381 do local row = altIds[id] check(row ~= nil and row.width == 18 and row.height == 17, "alt layout " .. id .. " is a baked 18x17 Trainer Tower floor") end print("[test] 3. ready() refuses a half-written group") local written = {} local stub = { write = function(_, rel, bytes) written[rel] = bytes end, read = function(_, rel) return written[rel] end, exists = function(_, rel) return written[rel] ~= nil end, } check(FameCheckerExtract.ready(stub, "x") == false, "fame ready() is false with nothing written") check(TeachyTvExtract.ready(stub, "x") == false, "teachy ready() is false with nothing written") check(MysteryGiftExtract.ready(stub, "x") == false, "gift ready() is false with nothing written") check(TrainerTowerExtract.ready(stub, "x") == false, "tower ready() is false with nothing written") check(TutorExtract.ready(stub, "x") == false, "tutor ready() is false with nothing written") for _, rel in ipairs({ "screen.rgba", "title.rgba", "manifest.lua" }) do written["x/teachy_tv/" .. rel] = "stub" end check(TeachyTvExtract.ready(stub, "x") == false, "teachy ready() is false without the end plate") written["x/teachy_tv/end.rgba"] = "stub" check(TeachyTvExtract.ready(stub, "x") == false, "teachy ready() also wants the snow and BG3 map") written["x/teachy_tv/static.rgba"] = "stub" written["x/teachy_tv/bg3.rgba"] = "stub" check(TeachyTvExtract.ready(stub, "x") == true, "teachy ready() accepts a complete group") written["x/mystery_gift/manifest.lua"] = "stub" for i = 0, 7 do written["x/mystery_gift/card_bg" .. i .. ".rgba"] = "stub" end check(MysteryGiftExtract.ready(stub, "x") == false, "gift ready() is false with the cards but no news") for i = 0, 7 do written["x/mystery_gift/news_bg" .. i .. ".rgba"] = "stub" end check(MysteryGiftExtract.ready(stub, "x") == true, "gift ready() accepts both background sets") print("[test] 4. a built cache carries every baked sheet") local Cache = require("tests.game3_cache") local root = Cache.root("fame_checker/manifest.lua") if not root then print("[skip] baked Fame Checker, Teachy TV, Mystery Gift and tower data: " .. tostring(Cache.reason)) finish() end print("[info] FireRed cache at " .. root) local function readFile(rel) local f = io.open(root .. "/" .. rel, "rb") if not f then return nil end local d = f:read("*a") f:close() return d end local function loadTable(rel) local src = readFile(rel) if not src then return nil end local chunk = (loadstring or load)(src) if not chunk then return nil end local ok, t = pcall(chunk) if ok and type(t) == "table" then return t end return nil end local blobs = {} for _, sheet in ipairs(SHEETS) do local rel, w, h = sheet[1], sheet[2], sheet[3] local blob = readFile(rel) blobs[rel] = blob eq(blob and #blob or nil, w * h * 4, rel .. " is " .. w .. "x" .. h) end local function pixel(blob, w, x, y) local o = (y * w + x) * 4 return blob:byte(o + 1), blob:byte(o + 2), blob:byte(o + 3), blob:byte(o + 4) end local function distinctColors(blob) local seen, n = {}, 0 for i = 1, #blob, 4 do local key = blob:sub(i, i + 3) if not seen[key] then seen[key] = true n = n + 1 end end return n end check(distinctColors(blobs["fame_checker/bg.rgba"]) > 4, "the Fame Checker page is painted art, not one flat colour") check(distinctColors(blobs["fame_checker/0.rgba"]) > 4, "the OAK portrait is painted art") local r, g, b = pixel(blobs["teachy_tv/screen.rgba"], 240, 0, 0) check(r == 255 and g == 230 and b == 90, string.format("the Teachy TV screen corner is the cart's cream (%d,%d,%d)", r, g, b)) -- src/teachy_tv.c:118 local cutOut = 0 for i = 4, #blobs["teachy_tv/screen.rgba"], 4 do if blobs["teachy_tv/screen.rgba"]:byte(i) == 0 then cutOut = cutOut + 1 end end check(cutOut > 1000, "the screen frame is a cut out, its tile-0 interior transparent (" .. cutOut .. " px)") local _, _, _, centreA = pixel(blobs["teachy_tv/screen.rgba"], 240, 120, 80) eq(centreA, 0, "the centre of the TV frame is transparent") -- src/teachy_tv.c:1219 TeachyTvLoadBg3Map renders Route1_Layout behind the cut out check(distinctColors(blobs["teachy_tv/bg3.rgba"]) > 8, "BG3 behind the cut out is the baked Route 1 map") local gr, gg, gb, ga = pixel(blobs["teachy_tv/bg3.rgba"], 256, 8, 120) check(ga == 255 and gg > gr and gg > gb, string.format("the Route 1 grass under the TV window is green (%d,%d,%d)", gr, gg, gb)) -- src/teachy_tv.c:637 tilemapBuffer[32 * i + j] = ((Random() & 3) << 10) + 0x301F local staticBlob = blobs["teachy_tv/static.rgba"] check(staticBlob:sub(1, 8 * 4) ~= staticBlob:sub(8 * 4 + 1, 16 * 4), "the four static frames are the four flips of tile 0x1F, not one repeated cel") local _, _, _, titleCentreA = pixel(blobs["teachy_tv/title.rgba"], 240, 120, 150) eq(titleCentreA, 0, "the title card is a cut out too, it is BG2 over the same frame") -- src/teachy_tv.c:1026 the end plate is 8x2 tiles at tile (20, 10) local _, _, _, cornerA = pixel(blobs["teachy_tv/end.rgba"], 240, 0, 0) eq(cornerA, 0, "the end plate is transparent outside its 8x2 block") local plateOpaque = 0 for y = 80, 95 do for x = 160, 223 do local _, _, _, pa = pixel(blobs["teachy_tv/end.rgba"], 240, x, y) if pa == 255 then plateOpaque = plateOpaque + 1 end end end check(plateOpaque > 200, "the end plate paints its block (" .. plateOpaque .. " opaque pixels)") check(blobs["mystery_gift/card_bg0.rgba"] ~= blobs["mystery_gift/card_bg7.rgba"], "wonder card backgrounds 0 and 7 are different art") -- src/mystery_gift_show_card.c:153 bg3 reuses sCard2Gfx / sCard2Map with its own palette check(blobs["mystery_gift/card_bg3.rgba"] ~= blobs["mystery_gift/card_bg0.rgba"], "wonder card 3 is card 2's tiles under its own palette") print("[test] 5. the Fame Checker text pack decodes to the cart's strings") local pack = loadTable("fame_checker/pack.lua") check(type(pack) == "table", "fame_checker/pack.lua loads") if pack then eq(pack.listNames[0], "OAK", "listNames[0] is gFameCheckerOakName") eq(pack.listNames[14], "FUJI", "listNames[14] is gFameCheckerMrFujiName") eq(pack.names[0], "PROF. OAK", "names[0] is gFameCheckerPersonName_ProfOak") eq(pack.names[15], "GIOVANNI", "names[15] is gFameCheckerPersonName_Giovanni") check(type(pack.quotes[0]) == "string" and pack.quotes[0]:find("{PLAYER}") ~= nil, "the OAK quote keeps its {PLAYER} placeholder") check(type(pack.flavorText[2]) == "table" and #tostring(pack.flavorText[2][0]) > 20, "BROCK slot 0 has flavour text") for p = 0, 15 do local rows = pack.flavorText[p] local locs = pack.originLocation[p] local objs = pack.originObject[p] if not (type(rows) == "table" and type(locs) == "table" and type(objs) == "table" and rows[5] and locs[5] and objs[5]) then check(false, "person " .. p .. " has six flavour texts with origins") end end check(true, "all sixteen people carry six flavour texts, locations and object names") end -- src/fame_checker.c:134 graphics/fame_checker/silhouette.gbapal is sixteen blacks local silhouette = readFile("fame_checker/silhouette.pal") eq(silhouette and #silhouette or nil, 16 * 3, "fame_checker/silhouette.pal is 16 colours") check(silhouette == string.rep("\0", 48), "the silhouette palette is the cart's all-black bank, so a locked person is black") local fameMan = loadTable("fame_checker/manifest.lua") check(type(fameMan) == "table", "fame_checker/manifest.lua loads") if fameMan then -- src/fame_checker.c:171 sFameCheckerTrainerPicIdxs eq(fameMan.trainerPic[3], 116, "trainerPic[1] is TRAINER_PIC_LEADER_BROCK") eq(fameMan.persons, 16, "the manifest names sixteen people") end print("[test] 6. the Trainer Tower set is gTrainerTowerFloors") local tower = loadTable("trainer_tower.lua") check(type(tower) == "table", "trainer_tower.lua loads") if tower then -- src/trainer_tower_sets.c:8951 eq(tower.header.numFloors, 8, "the local header is eight floors") eq(tower.header.id, 1, "the local header set id is 1") local floor = tower.floors[0][1] -- src/trainer_tower_sets.c:4793 sTrainerTowerFloor_Single_1 eq(floor.id, 19, "singles floor 1 is set 19") eq(floor.floorIdx, 8, "singles floor 1 carries MAX_TRAINER_TOWER_FLOORS") eq(floor.challengeType, 0, "singles floor 1 is CHALLENGE_TYPE_SINGLE") eq(floor.prize, 14, "singles floor 1 pays TTPRIZE_UP_GRADE") local trainer = floor.trainers[1] eq(trainer.name, "ALBERTO", "the first singles trainer is ALBERTO") eq(trainer.facilityClass, 91, "ALBERTO is FACILITY_CLASS_SAILOR") eq(trainer.textColor, 5, "ALBERTO's text colour is 5") eq(#trainer.speechBefore, 6, "the easy chat speech is six words") eq(#trainer.mons, 6, "every tower trainer carries PARTY_SIZE mons") local mon = trainer.mons[1] eq(mon.species, 160, "ALBERTO leads with FERALIGATR") eq(mon.heldItem, 196, "FERALIGATR holds a FOCUS BAND") eq(mon.moves[1], 57, "its first move is SURF") eq(mon.moves[2], 89, "its second move is EARTHQUAKE") eq(mon.nickname, "FERALIGATR", "the mon carries its nickname") check(mon.hpIV >= 0 and mon.hpIV <= 31, "the packed IVs decode into range (" .. mon.hpIV .. ")") local floors = 0 for challenge = 0, 3 do for i = 1, 8 do if tower.floors[challenge] and tower.floors[challenge][i] then floors = floors + 1 end end end eq(floors, 32, "all four challenge types carry eight floors") -- src/trainer_tower.c:105, :191 eq(tower.singlesTrainerInfo[91].objGfx, 62, "FACILITY_CLASS_SAILOR walks as OBJ_EVENT_GFX_SAILOR") eq(tower.singlesTrainerInfo[91].gender, 0, "the sailor is MALE") eq(tower.doublesTrainerInfo[126].objGfx1, 17, "FACILITY_CLASS_TWINS is two LITTLE_GIRLs") eq(tower.doublesTrainerInfo[126].objGfx2, 17, "both twins use the same graphics") -- src/trainer_tower.c:960, :382 eq(tower.encounterMusic[91], 285, "the sailor's encounter music is MUS_ENCOUNTER_BOY") eq(tower.facilityClassPic[91], 85, "FACILITY_CLASS_SAILOR draws TRAINER_PIC_SAILOR") end print("[test] 7. the egg groups and egg moves already in the cache are the cart's") local meta = loadTable("pokemon/meta.lua") local eggMoves = loadTable("pokemon/egg_moves.lua") check(type(meta) == "table", "pokemon/meta.lua loads") check(type(eggMoves) == "table", "pokemon/egg_moves.lua loads") if meta and eggMoves then -- src/data/pokemon/species_info.h:61 .eggGroups = {EGG_GROUP_MONSTER, EGG_GROUP_GRASS} eq(meta[1].eggGroup1, 1, "BULBASAUR is EGG_GROUP_MONSTER") eq(meta[1].eggGroup2, 7, "BULBASAUR is also EGG_GROUP_GRASS") eq(meta[25].eggGroup1, 5, "PIKACHU is EGG_GROUP_FIELD") eq(meta[25].eggGroup2, 6, "PIKACHU is also EGG_GROUP_FAIRY") eq(meta[132].eggGroup1, 13, "DITTO is EGG_GROUP_DITTO") eq(meta[151].eggGroup1, 15, "MEW is EGG_GROUP_NO_EGGS_DISCOVERED") eq(#eggMoves[1], 8, "BULBASAUR has eight egg moves") eq(eggMoves[1][1], 113, "BULBASAUR's first egg move is LIGHT SCREEN") eq(eggMoves[4][1], 187, "CHARMANDER's first egg move is BELLY DRUM") check(eggMoves[25] == nil, "PIKACHU has no egg move run of its own") end print("[test] 8. the tutor bitfield drives the real CanLearnTutorMove") local tutor = loadTable("pokemon/tutor.lua") check(type(tutor) == "table", "pokemon/tutor.lua loads") if tutor then -- src/data/pokemon/tutor_learnsets.h:1 eq(tutor.moves[0], 5, "tutor 0 is MOVE_MEGA_PUNCH") eq(tutor.moves[14], 164, "tutor 14 is MOVE_SUBSTITUTE") eq(tutor.learnsets[1], 0x409A, "BULBASAUR's tutor bits are the cart's 0x409A") eq(tutor.learnsets[0], nil, "SPECIES_NONE has no tutor bits") end Cache.mount("pokemon/tutor.lua") local MoveLearn = require("src.core.game3.move_learn") MoveLearn.resetTutorPack() check(MoveLearn.tutorLearnsets() ~= nil, "move_learn reads the baked tutor pack") -- src/party_menu.c:1907 CanLearnTutorMove check(MoveLearn.canLearnTutorMove(1, 1) == true, "BULBASAUR can learn SWORDS DANCE from the tutor") check(MoveLearn.canLearnTutorMove(1, 0) == false, "BULBASAUR cannot learn MEGA PUNCH from the tutor") check(MoveLearn.canLearnTutorMove(25, 11) == true, "PIKACHU can learn THUNDER WAVE from the tutor") -- src/data/pokemon/tutor_learnsets.h:969 check(MoveLearn.canLearnTutorMove(133, 7) == true, "EEVEE can learn MIMIC from the tutor") -- src/data/pokemon/tutor_learnsets.h:967 [SPECIES_DITTO] = 0 check(MoveLearn.canLearnTutorMove(132, 7) == false, "DITTO can learn no tutor move at all") -- src/party_menu.c:1910 the three starter ultimate moves are hard cases, not bits check(MoveLearn.canLearnTutorMove(3, 15) == true, "VENUSAUR can learn FRENZY PLANT") check(MoveLearn.canLearnTutorMove(6, 15) == false, "CHARIZARD cannot learn FRENZY PLANT") finish()