#!/usr/bin/env luajit package.path = "./?.lua;./?/init.lua;" .. package.path require("tests.fixture_data.game3_items").install() 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 romBundle = require("tests.game3_cache").bundle() if not romBundle then 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() return 0 end, list = function() return {} end, lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end, } end local fanfares = {} package.loaded["src.core.game3.audio"] = setmetatable({ playFanfare = function(id) fanfares[#fanfares + 1] = id return true end, waitFanfare = function(cb) if cb then cb() end end, }, { __index = function() return function() end end }) local Schema = require("src.core.game3.save_schema_firered") local session = Schema.newGame({ rngSeed = 0x1234 }) session.store = { flags = {}, vars = {} } package.loaded["src.core.game3.runtime"] = { getSession = function() return session end, isActive = function() return true end, } local MysteryGift = require("src.core.game3.mystery_gift") local Natives = require("src.core.game3.scripting.natives") local Gift = require("src.core.game3.scripting.natives_gift") local Bag = require("src.core.game3.bag") -- pokefirered/data/specials.inc:395 ValidateSavedWonderCard local SPECIAL_VALIDATE = Gift.SPECIAL_IDS.ValidateSavedWonderCard -- pokefirered/include/constants/flags.h:704 local FLAG_RECEIVED_MYSTIC_TICKET = 0x2A8 -- pokefirered/include/constants/flags.h:1408 local FLAG_ENABLE_SHIP_NAVEL_ROCK = 0x84A local function builtin(key) for _, entry in ipairs(MysteryGift.builtins()) do if entry.key == key then return entry end end return nil end local function newCtx() return { specialVars = {} } end local quiet = { log = function() end } print("[test] 1. The deliveryman shows only for a valid saved card") do MysteryGift.clear(session) local yield, value, known = Natives.special(newCtx(), SPECIAL_VALIDATE, quiet) check(known, "ValidateSavedWonderCard is bound") check(not yield, "it does not yield") eq(value, 0, "no saved card hides the deliveryman") local mystic = builtin("mystic_ticket") check(mystic ~= nil, "the built-in set carries the MYSTIC TICKET card") check(MysteryGift.receiveCard(session, mystic.card), "the card saves") local _, value2 = Natives.special(newCtx(), SPECIAL_VALIDATE, quiet) eq(value2, 1, "a saved card clears FLAG_HIDE_MG_DELIVERYMEN") end print("[test] 2. The once-only flag per card") do session.bag = Bag.new() MysteryGift.clear(session) session.store.flags = {} local mystic = builtin("mystic_ticket") MysteryGift.receiveCard(session, mystic.card) check(MysteryGift.isGiftNotReceived(session), "the gift starts uncollected") local first = MysteryGift.deliverGift(session) eq(first, MysteryGift.DELIVER_GIVEN, "the first visit hands the ticket over") eq(Bag.get(session.bag, MysteryGift.ITEM_MYSTIC_TICKET), 1, "one ticket in the bag") check(MysteryGift.getFlag(session, FLAG_ENABLE_SHIP_NAVEL_ROCK), "FLAG_ENABLE_SHIP_NAVEL_ROCK is set") check(MysteryGift.getFlag(session, FLAG_RECEIVED_MYSTIC_TICKET), "FLAG_RECEIVED_MYSTIC_TICKET is set") check(not MysteryGift.isGiftNotReceived(session), "the card reads as collected") local second = MysteryGift.deliverGift(session) eq(second, MysteryGift.DELIVER_ALREADY, "the second visit hands over nothing") eq(Bag.get(session.bag, MysteryGift.ITEM_MYSTIC_TICKET), 1, "still one ticket in the bag") end print("[test] 3. The received-gift flag alone closes a card") do session.bag = Bag.new() MysteryGift.clear(session) session.store.flags = {} local cave = builtin("altering_cave") MysteryGift.receiveCard(session, cave.card) local flagId = MysteryGift.getCardFlagId(session) local giftFlag = MysteryGift.receivedGiftFlag(flagId) check(giftFlag ~= nil, "the card maps into sReceivedGiftFlags") eq(MysteryGift.deliverGift(session), MysteryGift.DELIVER_GIVEN, "the first visit runs it") check(MysteryGift.getFlag(session, giftFlag), "and sets its sReceivedGiftFlags entry") eq(MysteryGift.deliverGift(session), MysteryGift.DELIVER_ALREADY, "a second visit does not") end print("[test] 4. trywondercardscript is the deliveryman's whole script") do session.bag = Bag.new() MysteryGift.clear(session) session.store.flags = {} local shown = nil local adapters = { log = function() end, playerName = "RED", openMessageAsync = function(text, done) shown = text done() end, } local ctx = newCtx() check(Gift.runWonderCardScript(ctx, adapters) == false, "with no saved card the script falls straight through") check(shown == nil, "and opens no message box") local mystic = builtin("mystic_ticket") MysteryGift.receiveCard(session, mystic.card) ctx = newCtx() Gift.runWonderCardScript(ctx, adapters) eq(Gift.lastDelivery, MysteryGift.DELIVER_GIVEN, "the deliveryman hands the gift over") check(Bag.has(session.bag, MysteryGift.ITEM_MYSTIC_TICKET, 1), "the ticket is in the bag") check(type(shown) == "table" or type(shown) == "string", "a message box was opened") shown = nil ctx = newCtx() Gift.runWonderCardScript(ctx, adapters) eq(Gift.lastDelivery, MysteryGift.DELIVER_ALREADY, "a second talk gives nothing") eq(Bag.get(session.bag, MysteryGift.ITEM_MYSTIC_TICKET), 1, "and no second ticket") end print("[test] 5. The deliveryman's four outcomes") do local already = Gift.deliveryText(session, MysteryGift.DELIVER_ALREADY) local noRoom = Gift.deliveryText(session, MysteryGift.DELIVER_NO_ROOM) local full = Gift.deliveryText(session, MysteryGift.DELIVER_PARTY_FULL) if romBundle then check(tostring(already):find("MYSTERY"), "the already-collected line thanks the player") -- pokefirered/data/mystery_event_msg.s:319 sText_MysticTicketNoPlace check(tostring(noRoom):find("KEY ITEMS POCKET", 1, true), "the no-room line names the KEY ITEMS POCKET") check(tostring(noRoom):find(tostring(session.name), 1, true), "and the player") check(tostring(full):find("party"), "the party-full line names the party") else eq(already, "sText_MysticTicketGot", "the mystic card's already-collected line") eq(noRoom, "sText_MysticTicketNoPlace", "the mystic card's no-room line") eq(full, "sText_FullParty", "the party-full line") end local given = Gift.deliveryText(session, MysteryGift.DELIVER_GIVEN) check(tostring(given):find("ticket"), "the handover line reads the card's own body text") -- pokefirered/data/mystery_event_msg.s:303 sText_MysticTicket2 local pages = {} for page in (tostring(given) .. "\\p"):gmatch("(.-)\\p") do pages[#pages + 1] = page end eq(#pages, 2, "four body lines become two message pages") for i, page in ipairs(pages) do local lines = 1 for _ in page:gmatch("\n") do lines = lines + 1 end eq(lines, 2, "page " .. i .. " is two lines high") end end print("[test] 6. The 0xCF opcode runs the card script through the VM") do local Vm = require("src.core.game3.scripting.vm") local Flags = require("src.core.game3.scripting.flags") local FALL_THROUGH = 0x300 local function runMan() session.bag = Bag.new() session.store = Flags.newStore() local shown = 0 local vm = Vm.new({ store = session.store, scripts = { -- pokefirered/data/scripts/cable_club.inc:15 CableClub_EventScript_MysteryGiftMan man = { { op = "trywondercardscript", opcode = 207 }, { op = "setflag", [1] = FALL_THROUGH, flag = FALL_THROUGH, opcode = 41 }, { op = "end", opcode = 3 }, }, }, adapters = { log = function() end, playerName = "RED", openMessageAsync = function(_, done) shown = shown + 1 done() end, }, }) vm:start("man") for _ = 1, 60 do if not vm:isRunning() then break end vm:tick() end return vm, shown end MysteryGift.clear(session) local vm1, shown1 = runMan() eq(shown1, 0, "no saved card opens no delivery message") check(Flags.getFlag(session.store, vm1.ctx, FALL_THROUGH), "and the script falls through to the rest of the deliveryman script") MysteryGift.clear(session) local mystic = builtin("mystic_ticket") MysteryGift.receiveCard(session, mystic.card) local vm2, shown2 = runMan() eq(shown2, 2, "a saved card opens the delivery message, then the obtained line") eq(Bag.get(session.bag, MysteryGift.ITEM_MYSTIC_TICKET), 1, "the opcode put the ticket in the bag") check(MysteryGift.getFlag(session, FLAG_ENABLE_SHIP_NAVEL_ROCK), "and set FLAG_ENABLE_SHIP_NAVEL_ROCK") -- pokefirered/src/scrcmd.c:280 ScriptJump check(not Flags.getFlag(session.store, vm2.ctx, FALL_THROUGH), "the card script jumped away instead of falling through") check(not vm2:isRunning(), "and the deliveryman script finished") end print("[test] 7. A shiny custom mon gift arrives shiny, with its fanfare") if not romBundle then print("[skip] no FireRed cache for the species data") else session.party = {} session.bag = Bag.new() MysteryGift.clear(session) session.store.flags = {} local otId = 0x12345678 local hi = 0xABCD local lo = bit.bxor(bit.bxor(0x5678, 0x1234), hi) local card = {} for k, v in pairs(builtin("surf_pichu").card) do card[k] = v end card.gift = { kind = "mon", species = 241, level = 20, personality = hi * 65536 + lo, otName = "AURA", otId = otId } check(MysteryGift.receiveCard(session, card), "the custom MILTANK card saves") local shown = {} local adapters = { log = function() end, playerName = "RED", openMessageAsync = function(text, done) shown[#shown + 1] = text done() end, } Gift.runWonderCardScript(newCtx(), adapters) eq(Gift.lastDelivery, MysteryGift.DELIVER_GIVEN, "the deliveryman hands the MILTANK over") local mon = session.party[#session.party] eq(mon and mon.species, 241, "a MILTANK joined the party") eq(mon and mon.otId, 0x5678, "its OT ID is the low half of the card's otId") eq(mon and mon.otSecretId, 0x1234, "its secret ID is the high half") local Pokemon = require("src.core.game3.pokemon") check(mon and Pokemon.isShiny(mon), "and it is shiny") eq(#shown, 2, "the card text is followed by an obtained line") check(type(Gift.lastObtained) == "string" and Gift.lastObtained:find("obtained", 1, true) ~= nil, "which says what was obtained") -- pokefirered/include/constants/songs.h:264 MUS_LEVEL_UP eq(fanfares[#fanfares], 257, "with the gift-mon fanfare") end print("[test] 8. A gift mon saved with the whole otId is split on load") do local SaveMon = require("src.core.game3.save_mon") local mon = { otId = 0x12345678, otSecretId = 999 } SaveMon.normalize(mon) eq(mon.otId, 0x5678, "the OT ID keeps the low half") eq(mon.otSecretId, 0x1234, "the secret ID takes the high half") end if failed == 0 then print("PASS game3_gift_delivery") else print("FAIL game3_gift_delivery failures=" .. failed) os.exit(1) end