-- Smoke: the whole Crystal boot chain, with no driver shortcut. -- -- POKEPORT_GAME=crystal POKEPORT_BOOT_CINEMA=1 \ -- POKEPORT_SHOT_DIR= \ -- POKEPORT_DRIVER=tests/drivers/crystal_boot_smoke.lua love . -- -- copyright -> GameFreak -> Crystal intro -> title -> intro menu -> NEW GAME -- -> clock -> Oak speech -> name pick -> naming screen -> the bedroom -> -- start menu -> outdoors -> a wild battle. Every step is asserted by the -- class of the state on the stack, so a broken hand-off names the screen it -- stalled on instead of just hanging. local U = require("tests.drivers.util") local CopyrightSplash = require("src.ui.gen2.CopyrightSplash") local CrystalIntro = require("src.ui.gen2.CrystalIntro") local GameFreakPresents = require("src.ui.gen2.GameFreakPresents") local GenderSelect = require("src.ui.gen2.GenderSelect") local InitClock = require("src.ui.gen2.InitClock") local MainMenu = require("src.ui.gen2.MainMenu") local NamePick = require("src.ui.gen2.NamePick") local NamingScreen = require("src.ui.gen2.NamingScreen") local OakSpeech = require("src.ui.gen2.OakSpeech") local OptionsMenu = require("src.ui.gen2.OptionsMenu") local StartMenu = require("src.ui.gen2.StartMenu") local TitleState = require("src.ui.gen2.TitleState") local GameVersion = require("src.core.GameVersion") local Mon = require("src.battle.gen2.Mon") return function(game) local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/crystal-boot" local log = io.open(os.getenv("POKEPORT_BOOT_LOG") or (out .. "/boot.log"), "w") local function say(line) print("[driver] " .. line) if log then log:write(line .. "\n"); log:flush() end end local function bail(reason) say("FAIL " .. reason) if log then log:close() end love.event.quit(1) error(reason, 0) end local function top() return game.stack:top() end local function isA(class) local state = top() return state ~= nil and getmetatable(state) == class end local function tap(button, frames) game.input.pressQueue[#game.input.pressQueue + 1] = button game.input.state[button] = true U.wait(2) game.input.state[button] = false U.wait(frames or 4) end local function waitFor(label, predicate, frames) for _ = 1, frames or 900 do if predicate() then return end U.wait(1) end bail(("stalled waiting for %s (top is %s)"):format(label, tostring(top()))) end local function pick(menu, value) for i, item in ipairs(menu.list.items) do if item.value == value then menu.list.index = i end end end if GameVersion.get() ~= "crystal" then bail("booted " .. tostring(GameVersion.get()) .. ", not crystal") end if GameVersion.engine() ~= "crystal" then bail("engine lineage is " .. tostring(GameVersion.engine())) end say("version=" .. GameVersion.get() .. " engine=" .. GameVersion.engine()) U.wait(10) if not isA(CopyrightSplash) then bail("boot did not start at the copyright splash (top " .. tostring(top()) .. ")") end say("OK copyright") U.shot(game, out .. "/01-copyright.png") waitFor("GameFreak presents", function() return isA(GameFreakPresents) end) U.wait(90) say("OK gamefreak") U.shot(game, out .. "/02-gamefreak.png") -- ../pokecrystal/engine/movie/intro.asm:1 waitFor("the Crystal intro", function() return isA(CrystalIntro) end) U.wait(240) say("OK crystal intro") U.shot(game, out .. "/03-intro.png") U.wait(240) U.shot(game, out .. "/03b-intro.png") tap("start") waitFor("the title screen", function() return isA(TitleState) end) U.wait(90) say("OK title") U.shot(game, out .. "/04-title.png") for _ = 1, 3 do tap("start", 3) end waitFor("the intro menu", function() return isA(MainMenu) end) say("OK main menu") U.shot(game, out .. "/05-mainmenu.png") local menu = top() pick(menu, "option") tap("a") waitFor("the options screen", function() return isA(OptionsMenu) end) U.shot(game, out .. "/06-options.png") tap("b") waitFor("the intro menu again", function() return isA(MainMenu) end) menu = top() pick(menu, "new") U.shot(game, out .. "/07-newgame.png") tap("a") -- ../pokecrystal/engine/menus/init_gender.asm:23 InitGender, which -- PlayerProfileSetup runs first (engine/menus/intro_menu.asm:80-84). waitFor("the gender screen", function() return isA(GenderSelect) end, 300) say("OK gender select") U.shot(game, out .. "/07b-gender.png") tap("a") -- ../pokecrystal/engine/menus/intro_menu.asm:628 waitFor("the clock screen", function() return isA(InitClock) end, 300) say("OK init clock") U.shot(game, out .. "/08-initclock.png") for _ = 1, 60 do if isA(OakSpeech) then break end tap("a", 2) end waitFor("the Oak speech", function() return isA(OakSpeech) end, 300) local oak = top() say("OK oak speech, demo mon = " .. tostring(oak.demoSpecies)) if oak.demoSpecies ~= "WOOPER" then bail("Oak's demo mon is " .. tostring(oak.demoSpecies) .. ", Crystal's is WOOPER") end U.wait(60) U.shot(game, out .. "/09-oak.png") local shotDemo = false for _ = 1, 400 do if isA(NamePick) then break end if not shotDemo and isA(OakSpeech) and top().pic == top().marillPic then U.wait(45) U.shot(game, out .. "/10-wooper.png") shotDemo = true end tap("a", 2) end if not isA(NamePick) then bail("Oak speech never reached the name picker (top " .. tostring(top()) .. ")") end say("OK name pick" .. (shotDemo and " (demo pic captured)" or "")) -- ../pokecrystal/engine/menus/intro_menu.asm:738 NamePlayer waitFor("the name menu to slide in", function() return top().slide == nil end, 240) U.shot(game, out .. "/11-namepick.png") local picker = top() picker.cursor = 1 tap("a") waitFor("the naming screen", function() return isA(NamingScreen) end) local naming = top() tap("a") tap("right") tap("a") if naming.text ~= "AB" then bail("typed name is " .. tostring(naming.text) .. ", expected AB") end say("OK naming screen") U.shot(game, out .. "/12-naming.png") naming.row = naming:bottomRow() naming.col = 6 tap("a") for _ = 1, 600 do if game.phase == "play" and game.world and game.world.map then break end tap("a", 2) end if not (game.phase == "play" and game.world and game.world.map) then bail("never reached the overworld (top is " .. tostring(top()) .. ")") end if game.save.player.name ~= "AB" then bail("player name is " .. tostring(game.save.player.name)) end if game.world.map.id ~= "PLAYERS_HOUSE_2F" then bail("new game started on " .. tostring(game.world.map.id) .. ", expected the bedroom") end say("OK overworld, map = " .. game.world.map.id .. ", name = " .. tostring(game.save.player.name)) U.wait(20) U.shot(game, out .. "/13-bedroom.png") tap("start") waitFor("the start menu", function() return isA(StartMenu) end) say("OK start menu") U.shot(game, out .. "/14-startmenu.png") tap("b") waitFor("the overworld again", function() return top() == nil end) if not game.world:setMap("NEW_BARK_TOWN", 13, 6, "down") then bail("could not load NEW_BARK_TOWN") end U.wait(30) if game.world.map.id ~= "NEW_BARK_TOWN" then bail("setMap landed on " .. tostring(game.world.map.id)) end local tileset = game.world.map.tileset or {} say("OK outdoors, tileset = " .. tostring(tileset.id) .. ", tilePalettes = " .. tostring(#(tileset.tilePalettes or {}))) U.shot(game, out .. "/15-newbark.png") local player = Mon.new(game.data, "CYNDAQUIL", 12) if not (player and #player.moves > 0) then bail("could not build a CYNDAQUIL from the Crystal pokemon.lua") end local wild = Mon.new(game.data, "WOOPER", 5) if not wild then bail("could not build a wild WOOPER") end game.save.party = { player } if not game.world:startBattle({ wild = wild }) then bail("startBattle failed") end U.wait(240) say("OK battle, " .. player.species .. " L" .. player.level .. " vs " .. wild.species .. " L" .. wild.level) U.shot(game, out .. "/16-battle.png") say("PASS crystal boot chain in " .. out) if log then log:close() end love.event.quit(0) end