#!/usr/bin/env luajit 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 love = love or require("tests.love_stub") local romBundle = require("tests.game3_cache").bundle() if not romBundle then local LIST_ROWS = { sListMenuItems_CardsOrNews = 3, sListMenuItems_ReceiveSendToss = 4, sListMenuItems_ReceiveToss = 3, sListMenuItems_WirelessOrFriend = 3, sListMenuItems_ReceiveSend = 3, sListMenuItems_Receive = 2 } package.loaded["src.core.game3.rom_text"] = { plain = function(key) return key end, box = function(key) return key end, ascii = function(key) return key end, has = function() return true end, ir = function(key) return { { t = "text", s = key } } end, key = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end, at = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end, count = function(n) return LIST_ROWS[n] or 0 end, list = function(n) local out = {} for i = 0, (LIST_ROWS[n] or 0) - 1 do out[i + 1] = n .. "[" .. i .. "]" end return out end, lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end, } end local function teq(a, b, msg) if romBundle then eq(a, b, msg) else print("[skip] ROM text: " .. msg) end end local function tcheck(cond, msg) if romBundle then check(cond, msg) else print("[skip] ROM text: " .. msg) end end local ListStub = {} ListStub.__index = ListStub ListStub.COLOR_WHITE = { fg = "ur_white" } function ListStub.new(opts) local lm = setmetatable({ opts = opts, items = opts.items, cursor = 1, draws = 0 }, ListStub) ListStub.last = lm return lm end function ListStub:handleInput(input) if input:wasPressed("down") and self.cursor < #self.items then self.cursor = self.cursor + 1 return "move" elseif input:wasPressed("up") and self.cursor > 1 then self.cursor = self.cursor - 1 return "move" elseif input:wasPressed("a") then return "select" elseif input:wasPressed("b") then return "cancel" end return nil end function ListStub:selected() return self.items[self.cursor], self.cursor end function ListStub:update() end function ListStub:draw() self.draws = self.draws + 1 end package.loaded["src.ui.game3.list_menu"] = ListStub local fanfares = {} package.loaded["src.core.game3.audio"] = setmetatable({ playFanfare = function(id) fanfares[#fanfares + 1] = id return true end, isFanfareFinished = function() return true end, }, { __index = function() return function() end end }) local Boot = require("src.ui.game3.boot") local Ui = require("src.ui.game3.mystery_gift") local MysteryGift = require("src.core.game3.mystery_gift") local WirelessIcon = require("src.ui.game3.wireless_icon") local FLAG_SYS_MYSTERY_GIFT_ENABLED = 0x839 local FIXTURE_PUB = "58c5b57e548789c3d231a9537f172bcf515a39b053f58a90c7b0096967a746ba" local FIXTURE_BODY = [==[{"v":1,"payload":"{\"cards\":[{\"bgType\":2,\"bodyText\":[\"Thank you for using the MYSTERY\",\"GIFT System.\",\"There is a ticket here for you.\",\"It is for use at VERMILION CITY port.\"],\"flagId\":1001,\"footerLine1Text\":\"Speak to the deliveryman\",\"footerLine2Text\":\"at a POKéMON CENTER.\",\"gift\":{\"haveFlags\":[680,754,755],\"item\":370,\"kind\":\"item\",\"quantity\":1,\"setFlags\":[2122,680]},\"iconSpecies\":0,\"idNumber\":1,\"key\":\"mystic_ticket\",\"maxStamps\":0,\"sendType\":0,\"subtitleText\":\"MYSTERY GIFT\",\"titleText\":\"MYSTIC TICKET\",\"type\":0},{\"bgType\":7,\"bodyText\":[\"A mythical POKéMON has been\",\"sent to you from the\",\"POKéMON CENTER.\",\"Please take good care of it.\"],\"flagId\":1007,\"footerLine1Text\":\"Speak to the deliveryman\",\"footerLine2Text\":\"at a POKéMON CENTER.\",\"gift\":{\"kind\":\"mon\",\"level\":10,\"moves\":{\"1\":1},\"otId\":20078,\"otName\":\"AURA\",\"species\":151},\"iconSpecies\":151,\"idNumber\":7,\"key\":\"mew\",\"maxStamps\":0,\"sendType\":0,\"subtitleText\":\"MYSTERY GIFT\",\"titleText\":\"MEW\",\"type\":0},{\"bgType\":4,\"bodyText\":[\"Thank you for using the STAMP CARD\",\"System.\",\"Trade with other TRAINERS to fill\",\"your STAMP CARD.\"],\"flagId\":1004,\"footerLine1Text\":\"Speak to the deliveryman\",\"footerLine2Text\":\"at a POKéMON CENTER.\",\"gift\":{\"kind\":\"none\"},\"iconSpecies\":0,\"idNumber\":4,\"key\":\"stamp_card\",\"maxStamps\":7,\"sendType\":1,\"subtitleText\":\"MYSTERY GIFT\",\"titleText\":\"STAMP CARD\",\"type\":1}],\"issued\":0,\"news\":[{\"bgType\":1,\"bodyText\":[\"Welcome to the POKéMON\",\"WIRELESS CLUB news.\",\"Look for new WONDER CARDS.\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"],\"id\":1,\"key\":\"launch_news\",\"sendType\":0,\"titleText\":\"WONDER NEWS\"}],\"v\":1}","sig":"44aa8146ee6d745efe81afc3427aaf84d07b4c64bacb6922c8d9ff27fa779e3d628c55cd623ab49c9afc2fc84f02be8942350cae4ae08c1e4e01955a671d7009","key":"58c5b57e548789c3d231a9537f172bcf515a39b053f58a90c7b0096967a746ba"}]==] local function transport(reply) local t = { calls = 0 } function t:begin() self.calls = self.calls + 1 return self.calls end function t:poll() if type(reply) == "function" then return reply() end return reply end function t:release() end function t:cancel() self.canceled = true end return t end local function okFeed() return transport({ status = "ok", code = 200, body = FIXTURE_BODY }) end MysteryGift.GIFT_PUBKEY = FIXTURE_PUB local function driver(st) local keys = {} local function pressed(k) return keys[k] == true end local d = {} function d.step(k) keys = {} if k then keys[k] = true end Ui.update(st, pressed, 1 / 60) end function d.waitFor(pred, key, limit) for _ = 1, limit or 2000 do if pred() then return true end if st.msg and st.msg.revealed >= st.msg.total and not st.msg.auto and not st.msg.hold then d.step(key or "a") else d.step(nil) end end return pred() end function d.finishMessage() for _ = 1, 2000 do if not st.msg then return end if st.msg.revealed >= st.msg.total and not st.msg.auto and not st.msg.hold then d.step("a") return end d.step(nil) end end return d end local function giftSession(save) local sess = MysteryGift.sessionFromSave(save) MysteryGift.enable(sess) return sess end local function openWireless(st, d, isNews) if isNews then d.step("down") end d.step("a") if st.state == Ui.STATE.SAVED_VIEW then d.step("a") d.step("a") end d.waitFor(function() return st.state == Ui.STATE.SEARCHING end) end print("[test] 1. The main menu rows") do local noSave = Boot.new() Boot.setHasContinue(noSave, false) local rows = Boot.menuItems(noSave) eq(#rows, 2, "no save file gives two rows") eq(rows[1], "NEW GAME", "row 1 is NEW GAME") local plain = Boot.new() Boot.setHasContinue(plain, true) Boot.setContinueInfo(plain, { name = "RED", mysteryGift = false }) rows = Boot.menuItems(plain) eq(#rows, 4, "any save file gets the MYSTERY GIFT row") eq(rows[3], "MYSTERY GIFT", "even without the passphrase flag") eq(rows[4], "EXIT", "EXIT follows it") check(Boot.hasMysteryGift(plain), "a save alone opens the Mystery Gift layout") local gift = Boot.new() Boot.setHasContinue(gift, true) Boot.setContinueInfo(gift, { name = "RED", mysteryGift = true }) rows = Boot.menuItems(gift) -- pokefirered/src/main_menu.c:370 MAIN_MENU_MYSTERYGIFT eq(#rows, 4, "the Mystery Gift layout keeps EXIT as a fourth row") eq(rows[1], "CONTINUE", "row 1 is CONTINUE") eq(rows[2], "NEW GAME", "row 2 is NEW GAME") eq(rows[3], "MYSTERY GIFT", "MYSTERY GIFT takes the third window") eq(rows[4], "EXIT", "EXIT is always on the menu") check(Boot.hasMysteryGift(gift), "the flag selects the Mystery Gift layout") -- pokefirered/src/main_menu.c:21 enum MainMenuType local sawOption = false for _, state in ipairs({ noSave, plain, gift }) do for _, row in ipairs(Boot.menuItems(state)) do if row == "OPTION" then sawOption = true end end end check(not sawOption, "no layout carries an OPTION row") end print("[test] 2. The flag comes off the save file") do local off = Boot.continueInfoFromSave({ name = "RED", flags = {} }) check(off.mysteryGift == false, "a save without the flag reads false") local on = Boot.continueInfoFromSave({ name = "RED", flags = { [tostring(FLAG_SYS_MYSTERY_GIFT_ENABLED)] = true }, }) check(on.mysteryGift == true, "FLAG_SYS_MYSTERY_GIFT_ENABLED reads true") end print("[test] 3. Picking MYSTERY GIFT reaches the front end") do local state = Boot.new() Boot.setHasContinue(state, true) Boot.setContinueInfo(state, { name = "RED", mysteryGift = true }) state.phase = Boot.PHASE.MENU state.menuIndex = 3 state.fadeT, state.fadeTarget = 0, 0 local key = nil local input = { wasPressed = function(_, k) return k == key end } key = "a" Boot.update(state, input, 1 / 60) eq(state.fadeThen, "mystery_gift", "the MYSTERY GIFT row starts the fade") key = nil for _ = 1, 400 do if state.phase == Boot.PHASE.MYSTERY_GIFT then break end Boot.update(state, input, 1 / 60) end eq(state.phase, Boot.PHASE.MYSTERY_GIFT, "the fade hands over to the Mystery Gift screen") check(type(state.gift) == "table", "a front-end state was built") eq(state.gift.state, Ui.STATE.MAIN_MENU, "the front end opens on its own menu") local okDraw, err = pcall(Boot.draw, state) tcheck(okDraw, "the Mystery Gift screen draws: " .. tostring(err)) key = "b" Boot.update(state, input, 1 / 60) eq(state.phase, Boot.PHASE.MENU, "B leaves the front end for the main menu") end print("[test] 4. The front end receives a card and saves it") local save, sess, st, saves do save = {} sess = giftSession(save) saves = 0 local feedTransport = okFeed() st = Ui.new({ session = sess, fetch = { transport = feedTransport }, onSave = function(s) saves = saves + 1 return MysteryGift.applyToSave(s, save) end, }) local d = driver(st) local step, finishMessage = d.step, d.finishMessage -- pokefirered/src/mystery_gift_menu.c:197 sListMenuItems_CardsOrNews local rows = Ui.mainRows() teq(rows[1], "WONDER CARDS", "the front end opens on WONDER CARDS") teq(rows[2], "WONDER NEWS", "WONDER NEWS is the second row") teq(rows[3], "EXIT", "EXIT is the third row") step("a") -- pokefirered/src/mystery_gift_menu.c:1147 MG_STATE_DONT_HAVE_ANY eq(st.state, Ui.STATE.RESULT_MSG, "with no card held the don't-have text comes first") eq(feedTransport.calls, 0, "nothing is fetched yet") d.waitFor(function() return st.state == Ui.STATE.SEARCHING end) eq(st.state, Ui.STATE.SEARCHING, "then WONDER CARDS fetches the card list") eq(feedTransport.calls, 1, "one request goes out") eq(WirelessIcon.anim(), "searching", "the wireless icon plays its searching anim") tcheck(type(st.prompt) == "string" and st.prompt:find("Searching for a WIRELESS"), "with the cart's searching text") d.waitFor(function() return st.state ~= Ui.STATE.SEARCHING end) eq(st.state, Ui.STATE.OFFER_LIST, "the verified feed opens the card list") eq(WirelessIcon.anim(), "3bars", "the icon shows the link") local lm = ListStub.last check(lm ~= nil and lm.opts.template.left == 1 and lm.opts.template.top == 3, "the list sits at sWindowTemplate_GroupList") eq(lm and #lm.items, 3, "every card in the feed is listed") eq(lm and lm.items[1].label, "MYSTIC TICKET", "row 1 is the MYSTIC TICKET") eq(lm and lm.items[2].label, "MEW", "row 2 is MEW") eq(lm and lm.opts.maxShowed, 5, "five rows show at once") step("a") eq(st.state, Ui.STATE.GIFT_INPUT, "pressing a card shows it first") eq(Ui.card(st).titleText, "MYSTIC TICKET", "the preview is the picked card") check(not MysteryGift.validateSavedCard(sess), "a preview installs nothing") local okDraw, err = pcall(Ui.draw, st) tcheck(okDraw, "the preview draws: " .. tostring(err)) step("a") eq(st.state, Ui.STATE.RESULT_MSG, "A receives it at once") eq(WirelessIcon.anim(), nil, "the icon goes away when the link ends") check(MysteryGift.validateSavedCard(sess), "the card is installed") tcheck(st.msg ~= nil and tostring(st.msg.text):find("WONDER CARD"), "the received message names the card") check(st.msg ~= nil and st.msg.hold == Ui.SUCCESS_FRAMES, "the success message holds for the fanfare") eq(fanfares[#fanfares], 258, "MUS_OBTAIN_ITEM plays") d.waitFor(function() return st.state == Ui.STATE.SAVE and not st.msg end) eq(st.state, Ui.STATE.SAVE, "and the game saves") step(nil) eq(st.state, Ui.STATE.SAVE_DONE, "the save runs once") eq(saves, 1, "onSave was called exactly once") finishMessage() eq(st.state, Ui.STATE.MAIN_MENU, "then back to the Mystery Gift menu") check(MysteryGift.validateSavedCard(sess), "the card is on the session") check(type(save.modData) == "table" and type(save.modData.mysteryGift) == "table", "the record reached the save table") check(save.flags[tostring(FLAG_SYS_MYSTERY_GIFT_ENABLED)] == true, "FLAG_SYS_MYSTERY_GIFT_ENABLED reached the save table") end print("[test] 5. The list marks the held card and opens it") do local d = driver(st) d.step("a") -- pokefirered/src/mystery_gift_menu.c:1390 MG_STATE_LOAD_GIFT eq(st.state, Ui.STATE.SAVED_VIEW, "WONDER CARDS shows the held card first") eq(Ui.card(st).titleText, "MYSTIC TICKET", "the saved card is on screen") local okSaved, errSaved = pcall(Ui.draw, st) tcheck(okSaved, "the saved card draws: " .. tostring(errSaved)) d.step("a") eq(st.state, Ui.STATE.GIFT_MENU, "A opens the card's menu") eq(#st.rows, 3, "a keep-only card offers RECEIVE, TOSS and CANCEL") eq(st.giftActions[2], "toss", "the second row tosses") local okMenu, errMenu = pcall(Ui.draw, st) tcheck(okMenu, "the card menu draws: " .. tostring(errMenu)) d.step("b") eq(st.state, Ui.STATE.SAVED_VIEW, "B goes back to the card") d.step("a") d.step("a") eq(st.state, Ui.STATE.SEARCHING, "RECEIVE fetches again") d.waitFor(function() return st.state ~= Ui.STATE.SEARCHING end) eq(st.state, Ui.STATE.OFFER_LIST, "the list opens") local lm = ListStub.last -- pokefirered/src/union_room.c:4072 eq(lm and lm.items[1].colors, ListStub.COLOR_WHITE, "the MYSTIC TICKET shows as already had") eq(lm and lm.items[2].colors, nil, "MEW does not") d.step("a") eq(st.state, Ui.STATE.GIFT_INPUT, "pressing the held card opens it") local card = Ui.card(st) eq(card.titleText, "MYSTIC TICKET", "the saved card is the MYSTIC TICKET") eq(#card.bodyText, 4, "the card carries four body lines") local okDraw, err = pcall(Ui.draw, st) tcheck(okDraw, "the card view draws: " .. tostring(err)) d.step("b") eq(st.state, Ui.STATE.OFFER_LIST, "B goes back to the list") d.step("b") eq(st.state, Ui.STATE.MAIN_MENU, "B on the list goes back to the menu") check(MysteryGift.validateSavedCard(sess), "the held card stays") end print("[test] 6. Wonder News paging") do local body = {} for i = 1, 10 do body[i] = "LINE " .. i end local nst = Ui.new({ session = giftSession({}), onSave = function() return true end }) local keys = {} local function pressed(k) return keys[k] == true end local function step(k) keys = {} if k then keys[k] = true end Ui.update(nst, pressed, 1 / 60) end nst.isNews = true nst.viewNews = { id = 7, bgType = 1, titleText = "WONDER NEWS", bodyText = body } nst.newsScroll = 0 nst.state = Ui.STATE.NEWS_VIEW step("down") eq(nst.newsScroll, 1, "down scrolls one line") step("down") eq(nst.newsScroll, 2, "down scrolls again") step("down") -- pokefirered/src/mystery_gift_show_news.c:346 scrollEnd counts lines past the eighth eq(nst.newsScroll, 2, "ten lines over an eight-line window stop at two") step("up") eq(nst.newsScroll, 1, "up scrolls back") local okDraw, err = pcall(Ui.draw, nst) tcheck(okDraw, "the news view draws: " .. tostring(err)) step("b") eq(nst.state, Ui.STATE.OFFER_LIST, "B goes back to the news list") check(nst.viewNews == nil, "and drops the news it showed") end local function freshScreen(t, save) save = save or {} local sess2 = giftSession(save) local st2 = Ui.new({ session = sess2, fetch = { transport = t }, onSave = function(s) return MysteryGift.applyToSave(s, save) end, }) return st2, driver(st2), sess2, save end local function searchResult(st2, d) d.waitFor(function() return st2.state ~= Ui.STATE.SEARCHING end) end print("[test] 7. Wonder News over WIRELESS COMMUNICATION") do local st2, d, sess2 = freshScreen(okFeed()) openWireless(st2, d, true) eq(st2.state, Ui.STATE.SEARCHING, "WONDER NEWS fetches the list") searchResult(st2, d) eq(st2.state, Ui.STATE.OFFER_LIST, "the news list opens") eq(#ListStub.last.items, 1, "only news rows are listed") eq(ListStub.last.items[1].label, "WONDER NEWS", "the news title is the row") d.step("a") eq(st2.state, Ui.STATE.NEWS_VIEW, "pressing a row opens that news") eq(st2.viewNews and st2.viewNews.titleText, "WONDER NEWS", "the picked news is the one shown") check(not MysteryGift.validateSavedNews(sess2), "reading saves nothing") d.step("b") eq(st2.state, Ui.STATE.OFFER_LIST, "B goes back to the list") d.step("b") eq(st2.state, Ui.STATE.MAIN_MENU, "B on the list goes back to the menu") end print("[test] 8. Cards you had before stay marked and can be switched back to") do local save = {} local st2, d, sess2 = freshScreen(okFeed(), save) openWireless(st2, d, false) searchResult(st2, d) d.step("down") d.step("a") d.step("a") d.waitFor(function() return st2.state == Ui.STATE.MAIN_MENU and not st2.msg end) eq(MysteryGift.getSavedCard(sess2).idNumber, 7, "the MEW card is installed") check(MysteryGift.isGiftNotReceived(sess2), "the MEW is not collected yet") local rec = MysteryGift.ensure(sess2) rec.deliveredIds = { 7 } check(MysteryGift.wasDelivered(sess2, MysteryGift.getSavedCard(sess2)), "a Pokemon card is tracked by its ID number") MysteryGift.clearCardAndRelated(sess2) check(not MysteryGift.validateSavedCard(sess2), "then tossed") openWireless(st2, d, false) searchResult(st2, d) eq(ListStub.last.items[2].colors, ListStub.COLOR_WHITE, "the claimed card prints greyed in the list") d.step("down") d.step("a") d.step("a") d.waitFor(function() return st2.state == Ui.STATE.MAIN_MENU and not st2.msg end) eq(MysteryGift.getSavedCard(sess2).idNumber, 7, "a card you had before installs again") check(not MysteryGift.isGiftNotReceived(sess2), "and a gift already collected stays collected") local reopened, d2, sess3 = freshScreen(okFeed(), save) check(MysteryGift.hasClaimedCard(sess3, 7), "the claim was saved with the game") openWireless(reopened, d2, false) searchResult(reopened, d2) eq(ListStub.last.items[2].colors, ListStub.COLOR_WHITE, "and still prints greyed after a reload") d2.step("a") d2.step("a") d2.waitFor(function() return reopened.state == Ui.STATE.ASK_REPLACE end) d2.step("a") if reopened.state == Ui.STATE.ASK_REPLACE_UNRECEIVED then d2.step("a") end d2.waitFor(function() return reopened.state == Ui.STATE.SAVE end) eq(MysteryGift.getSavedCard(sess3).idNumber, 1, "switching to another card replaces the held one") end print("[test] 9. Replacing a held card asks first") do local st2, d, sess2 = freshScreen(okFeed()) MysteryGift.receiveCard(sess2, MysteryGift.builtins()[2].card) openWireless(st2, d, false) eq(st2.state, Ui.STATE.SEARCHING, "WONDER CARDS fetches the list") searchResult(st2, d) d.step("a") d.step("a") d.waitFor(function() return st2.state == Ui.STATE.ASK_REPLACE end) check(st2.yesno ~= nil, "the cart's throw-away question is up") d.step("a") eq(st2.state, Ui.STATE.ASK_REPLACE_UNRECEIVED, "an uncollected gift asks again") d.step("b") eq(st2.state, Ui.STATE.OFFER_LIST, "declining goes back to the list") eq(MysteryGift.getSavedCard(sess2).idNumber, 2, "the held card stays") end print("[test] 10. Offline, forged and empty feeds") do local st2, d, sess2 = freshScreen(transport({ status = "error", err = "could not connect" })) openWireless(st2, d, false) searchResult(st2, d) eq(st2.state, Ui.STATE.RESULT_MSG, "no network ends the search") eq(WirelessIcon.anim(), "error", "the icon shows the error") tcheck(st2.msg ~= nil and tostring(st2.msg.text):find("Wireless Adapter"), "with the cart's not-connected text") d.waitFor(function() return st2.state == Ui.STATE.MAIN_MENU end) eq(st2.state, Ui.STATE.MAIN_MENU, "and it goes back to the menu") eq(WirelessIcon.anim(), nil, "with the icon gone") local body = FIXTURE_BODY:gsub('\\"item\\":370', '\\"item\\":371') check(body ~= FIXTURE_BODY, "the forged body differs") local forged, fd, fsess = freshScreen(transport({ status = "ok", code = 200, body = body })) openWireless(forged, fd, false) searchResult(forged, fd) eq(forged.state, Ui.STATE.RESULT_MSG, "a forged feed ends the search") eq(forged.lastError, "bad_signature", "because the signature failed") tcheck(forged.msg ~= nil and tostring(forged.msg.text):find("error"), "with the cart's communication error") check(not MysteryGift.validateSavedCard(fsess), "and nothing is installed") fd.waitFor(function() return forged.state == Ui.STATE.MAIN_MENU and not forged.msg end) eq(forged.state, Ui.STATE.MAIN_MENU, "back to the Mystery Gift menu") MysteryGift.GIFT_PUBKEY = "2cecc61cc4d4ea70fc6802a66643e659a0f2c344eec6391ccf20b434ab280ffd" local real, rd, rsess = freshScreen(okFeed()) openWireless(real, rd, false) searchResult(real, rd) eq(real.lastError, "bad_signature", "a feed not signed by the owner's key is refused") check(not MysteryGift.validateSavedCard(rsess), "and installs nothing") MysteryGift.GIFT_PUBKEY = FIXTURE_PUB local noNews = [==[{"v":1,"payload":"{\"cards\":[],\"issued\":0,\"news\":[],\"v\":1}","sig":"4725a7934bfa21b943699b2b155d55235f598a7cc75531c4da36123848e296b590636ed33c3fcc7c4e19b0f604f5ea6e47fd5102676e365bfadfa5d6567a1c01"}]==] MysteryGift.GIFT_PUBKEY = "6130adae42fe6c943b91dc721db1cc1ba2296de882f919b2380f6fc3a2f4cec8" local empty, ed = freshScreen(transport({ status = "ok", code = 200, body = noNews })) openWireless(empty, ed, true) searchResult(empty, ed) MysteryGift.GIFT_PUBKEY = FIXTURE_PUB eq(empty.state, Ui.STATE.RESULT_MSG, "an empty news set ends the search") -- pokefirered/src/union_room.c:2559 tcheck(empty.msg ~= nil and tostring(empty.msg.text):find("NEWS"), "with the cart's no-news-shared text") ed.waitFor(function() return empty.state == Ui.STATE.MAIN_MENU end) eq(empty.state, Ui.STATE.MAIN_MENU, "and it goes back to the menu") local slow = transport({ status = "pending" }) local canceled, cd = freshScreen(slow) openWireless(canceled, cd, false) for _ = 1, 10 do cd.step(nil) end cd.step("b") check(slow.canceled == true, "B cancels the request") eq(canceled.state, Ui.STATE.RESULT_MSG, "B stops the search") tcheck(canceled.msg ~= nil and tostring(canceled.msg.text):find("canceled"), "with the cart's search-canceled text") end print("[test] 11. The save message runs the save with no button press") do local saves = 0 local st2 = Ui.new({ session = giftSession({}), fetch = { transport = okFeed() }, onSave = function() saves = saves + 1 return true end, }) local d = driver(st2) local step = d.step openWireless(st2, d, false) searchResult(st2, d) step("a") step("a") d.waitFor(function() return st2.state == Ui.STATE.RESULT_MSG end) eq(st2.state, Ui.STATE.RESULT_MSG, "the card arrives") for _ = 1, 1200 do if st2.state ~= Ui.STATE.RESULT_MSG then break end step(nil) end eq(st2.state, Ui.STATE.SAVE, "the save message is up without a button press") -- pokefirered/src/mystery_gift_menu.c:860 SaveOnMysteryGiftMenu for _ = 1, 600 do if saves > 0 then break end step(nil) end eq(saves, 1, "the save ran with no button press") eq(st2.state, Ui.STATE.SAVE_DONE, "and the completed message follows") tcheck(st2.msg ~= nil and tostring(st2.msg.text):find("press the A Button"), "which is the one that waits for A") end print("[test] 12. EXIT stays on the menu under the MYSTERY GIFT row") do local gift = Boot.new() Boot.setHasContinue(gift, true) Boot.setContinueInfo(gift, { name = "RED", mysteryGift = true }) gift.phase = Boot.PHASE.MENU gift.fadeT, gift.fadeTarget = 0, 0 local rows = Boot.menuItems(gift) local sawExitRow = false for _, row in ipairs(rows) do if row == "EXIT" then sawExitRow = true end end check(sawExitRow, "the EXIT row is still there with MYSTERY GIFT on the menu") local key = "select" Boot.update(gift, { wasPressed = function(_, k) return k == key end }, 1 / 60) check(gift.fadeThen ~= "exit", "SELECT is not a hidden exit") for _ = 1, 3 do key = "down" Boot.update(gift, { wasPressed = function(_, k) return k == key end, isDown = function() return false end }, 1 / 60) end eq(gift.menuIndex, 4, "three downs reach the EXIT row") check((gift.menuScroll or 0) > 0, "and the menu scrolled to show it") key = "a" Boot.update(gift, { wasPressed = function(_, k) return k == key end }, 1 / 60) eq(gift.fadeThen, "exit", "A on EXIT starts the exit fade") key = nil local action = nil for _ = 1, 400 do action = Boot.update(gift, { wasPressed = function(_, k) return k == key end }, 1 / 60) if action then break end end eq(type(action) == "table" and action.action or nil, "exit", "and it reaches the launcher exit action") end print("[test] 13. WONDER CARDS waits for the questionnaire passphrase") do local locked = Ui.new({ session = MysteryGift.sessionFromSave({}), onSave = function() return true end }) eq(#locked.rows, 2, "without the passphrase only WONDER NEWS and EXIT show") teq(locked.rows[1], "WONDER NEWS", "row 1 is WONDER NEWS") teq(locked.rows[2], "EXIT", "row 2 is EXIT") Ui.update(locked, function(k) return k == "a" end, 1 / 60) check(locked.isNews, "the first row opens the news branch") eq(locked.state, Ui.STATE.RESULT_MSG, "which says no news is held before fetching") local open = Ui.new({ session = giftSession({}), onSave = function() return true end }) eq(#open.rows, 3, "the passphrase adds WONDER CARDS back") teq(open.rows[1], "WONDER CARDS", "on the first row") end print("[test] 14. Receiving Wonder News from the relay saves it and sets the reward") do local save = {} local st2, d, sess2 = freshScreen(okFeed(), save) local before = #fanfares openWireless(st2, d, true) searchResult(st2, d) d.step("a") eq(st2.state, Ui.STATE.NEWS_VIEW, "the news opens") d.step("a") eq(st2.state, Ui.STATE.RESULT_MSG, "A receives it") check(MysteryGift.validateSavedNews(sess2), "the news is saved") check(MysteryGift.hasClaimedNews(sess2, 1), "and claimed") eq(#fanfares, before + 1, "the received fanfare plays") tcheck(st2.msg ~= nil and tostring(st2.msg.text):find("NEWS"), "with the news-received text") eq(MysteryGift.getSavedNewsMetadata(sess2).newsType, MysteryGift.WONDER_NEWS_NONE, "the reward is not set before the message ends") d.waitFor(function() return st2.state == Ui.STATE.SAVE end) -- pokefirered/src/mystery_gift_menu.c:1369 WonderNews_SetReward(WONDER_NEWS_RECV_WIRELESS) eq(MysteryGift.getSavedNewsMetadata(sess2).newsType, MysteryGift.WONDER_NEWS_RECV_WIRELESS, "the wireless reward is set once the message is done") d.waitFor(function() return st2.state == Ui.STATE.MAIN_MENU and not st2.msg end) check(type(save.modData) == "table" and save.modData.mysteryGift.news ~= nil, "the news reached the save") openWireless(st2, d, true) eq(st2.state, Ui.STATE.SEARCHING, "the saved news offers RECEIVE") searchResult(st2, d) eq(ListStub.last.items[1].colors, ListStub.COLOR_WHITE, "the held news prints greyed") MysteryGift.getSavedNewsMetadata(sess2).newsType = MysteryGift.WONDER_NEWS_NONE d.step("a") d.step("a") eq(st2.state, Ui.STATE.RESULT_MSG, "the same news again ends the link") tcheck(st2.msg ~= nil and tostring(st2.msg.text):find("already"), "with the already-had text") d.waitFor(function() return st2.state == Ui.STATE.MAIN_MENU end) eq(MysteryGift.getSavedNewsMetadata(sess2).newsType, MysteryGift.WONDER_NEWS_NONE, "already-had news earns no reward") end print("[test] 15. The saved card menu tosses a card") do local save = {} local saves = 0 local sess2 = giftSession(save) local st2 = Ui.new({ session = sess2, fetch = { transport = okFeed() }, onSave = function(s) saves = saves + 1 return MysteryGift.applyToSave(s, save) end }) local d = driver(st2) check(MysteryGift.receiveCard(sess2, MysteryGift.builtins()[2].card), "an uncollected AURORA card is held") d.step("a") eq(st2.state, Ui.STATE.SAVED_VIEW, "the held card shows") d.step("a") d.step("down") d.step("a") eq(st2.state, Ui.STATE.ASK_TOSS, "TOSS asks first") check(st2.yesno ~= nil, "with a yes/no") d.step("a") check(st2.yesno ~= nil and st2.state == Ui.STATE.ASK_TOSS, "an uncollected gift asks a second time") d.step("b") eq(st2.state, Ui.STATE.GIFT_MENU, "NO goes back to the menu") check(MysteryGift.validateSavedCard(sess2), "and keeps the card") d.step("down") d.step("a") d.step("a") d.step("a") check(not MysteryGift.validateSavedCard(sess2), "YES twice throws the card away") d.waitFor(function() return st2.state == Ui.STATE.MAIN_MENU and not st2.msg end) eq(saves, 1, "the toss is saved once") check(save.modData.mysteryGift.card == nil, "the save has no card") end print("[test] 16. A shareable card has no SEND row") do local sess2 = giftSession({}) local st2 = Ui.new({ session = sess2, onSave = function() return true end }) local d = driver(st2) check(MysteryGift.receiveCard(sess2, MysteryGift.builtins()[4].card), "a shareable STAMP CARD is held") d.step("a") d.step("a") eq(#st2.rows, 3, "the menu is RECEIVE, TOSS and CANCEL") eq(table.concat(st2.giftActions, ","), "receive,toss,cancel", "with no send action") check(MysteryGift.sendToFriend == nil, "there is no friend-send hook") d.step("b") d.step("b") eq(st2.state, Ui.STATE.MAIN_MENU, "B twice goes back to the menu") end print("[test] 17. Held news offers RECEIVE and CANCEL only") do local sess2 = giftSession({}) local st2 = Ui.new({ session = sess2, onSave = function() return true end }) local d = driver(st2) check(MysteryGift.receiveNews(sess2, { id = 3, sendType = 1, titleText = "NEWS", bodyText = {} }), "shareable news is held") d.step("down") d.step("a") eq(st2.state, Ui.STATE.SAVED_VIEW, "the held news shows") d.step("a") eq(table.concat(st2.giftActions, ","), "receive,cancel", "news has no send or toss") end if failed == 0 then print("PASS game3_gift_menu") else print("FAIL game3_gift_menu failures=" .. failed) os.exit(1) end