#!/usr/bin/env luajit package.path = "./?.lua;./?/init.lua;" .. package.path require("tests.game3_cache").requireData("game3_gift_model_test") 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 MysteryGift = require("src.core.game3.mystery_gift") local Schema = require("src.core.game3.save_schema_firered") local Bag = require("src.core.game3.bag") local FLAG_ENABLE_SHIP_NAVEL_ROCK = 0x84A local FLAG_ENABLE_SHIP_BIRTH_ISLAND = 0x84B local FLAG_RECEIVED_MYSTIC_TICKET = 0x2A8 local FLAG_RECEIVED_AURORA_TICKET = 0x2A7 local function newSession() return Schema.newGame({ rngSeed = 0x1234 }) end local function builtin(key) for _, entry in ipairs(MysteryGift.builtins()) do if entry.key == key then return entry end end return nil end print("[test] 1. CalcCRC16WithTable") -- pokefirered/src/util.c:250 CalcCRC16WithTable eq(MysteryGift.crc16(""), 0xEEDE, "crc16 of an empty record") check(MysteryGift.crc16("MYSTIC") ~= MysteryGift.crc16("AURORA"), "crc16 separates two records") print("[test] 2. Card and news round trip through a save") local session = newSession() check(MysteryGift.receiveCard(session, builtin("mystic_ticket").card), "the Mystic Ticket card saves") check(MysteryGift.receiveNews(session, { id = 7, sendType = MysteryGift.SEND_TYPE_ALLOWED, bgType = 1, titleText = "BERRY NEWS", bodyText = { "A friend sent you news." }, }, MysteryGift.WONDER_NEWS_RECV_FRIEND), "the Wonder News saves") local restored = Schema.fromSaveTable(Schema.toSaveTable(session)) check(MysteryGift.validateSavedCard(restored), "the saved card survives the save round trip") check(MysteryGift.validateSavedNews(restored), "the saved news survives the save round trip") eq(MysteryGift.getCardFlagId(restored), MysteryGift.WONDER_CARD_FLAG_OFFSET + 1, "the restored card keeps its flagId") eq(MysteryGift.getSavedCard(restored).titleText, "MYSTIC TICKET", "the restored card keeps its title") eq(MysteryGift.getSavedNews(restored).id, 7, "the restored news keeps its id") check(MysteryGift.isEnabled(restored), "FLAG_SYS_MYSTERY_GIFT_ENABLED survives the round trip") print("[test] 3. Validation rejects a malformed card") -- pokefirered/src/mystery_gift.c:191 ValidateWonderCard local good = builtin("aurora_ticket").card local function reject(mutate, label) local card = MysteryGift.normalizeCard(good) mutate(card) local s = newSession() check(not MysteryGift.saveCard(s, card), label) check(not MysteryGift.validateSavedCard(s), label .. " leaves no saved card") end reject(function(c) c.flagId = 0 end, "flagId 0 is rejected") reject(function(c) c.type = MysteryGift.CARD_TYPE_COUNT end, "an out of range card type is rejected") reject(function(c) c.sendType = 3 end, "an unknown sendType is rejected") reject(function(c) c.bgType = MysteryGift.NUM_WONDER_BGS end, "an out of range bgType is rejected") reject(function(c) c.maxStamps = MysteryGift.MAX_STAMP_CARD_STAMPS + 1 end, "more stamps than the card can hold is rejected") local tampered = newSession() MysteryGift.receiveCard(tampered, good) check(MysteryGift.validateSavedCard(tampered), "the aurora card validates before tampering") MysteryGift.getSavedCard(tampered).titleText = "FREE MASTER BALL" check(not MysteryGift.validateSavedCard(tampered), "an edited card fails its CRC") print("[test] 4. The tickets set the two ship flags") -- pokefirered/data/mystery_event_msg.s:266 MysteryEventScript_MysticTicket local mystic = newSession() MysteryGift.receiveCard(mystic, builtin("mystic_ticket").card) check(MysteryGift.isGiftNotReceived(mystic), "the Mystic Ticket gift is pending before delivery") eq(MysteryGift.deliverGift(mystic), MysteryGift.DELIVER_GIVEN, "the Mystic Ticket is handed over") check(Bag.has(mystic.bag, MysteryGift.ITEM_MYSTIC_TICKET, 1), "the MYSTIC TICKET is in the bag") check(MysteryGift.getFlag(mystic, FLAG_ENABLE_SHIP_NAVEL_ROCK), "FLAG_ENABLE_SHIP_NAVEL_ROCK is set") check(MysteryGift.getFlag(mystic, FLAG_RECEIVED_MYSTIC_TICKET), "FLAG_RECEIVED_MYSTIC_TICKET is set") check(not MysteryGift.getFlag(mystic, FLAG_ENABLE_SHIP_BIRTH_ISLAND), "the Mystic Ticket leaves Birth Island alone") check(not MysteryGift.isGiftNotReceived(mystic), "the card reads as collected afterwards") eq(MysteryGift.deliverGift(mystic), MysteryGift.DELIVER_ALREADY, "a second visit hands over nothing") -- pokefirered/data/mystery_event_msg.s:208 MysteryEventScript_AuroraTicket local aurora = newSession() MysteryGift.receiveCard(aurora, builtin("aurora_ticket").card) eq(MysteryGift.deliverGift(aurora), MysteryGift.DELIVER_GIVEN, "the Aurora Ticket is handed over") check(Bag.has(aurora.bag, MysteryGift.ITEM_AURORA_TICKET, 1), "the AURORA TICKET is in the bag") check(MysteryGift.getFlag(aurora, FLAG_ENABLE_SHIP_BIRTH_ISLAND), "FLAG_ENABLE_SHIP_BIRTH_ISLAND is set") check(MysteryGift.getFlag(aurora, FLAG_RECEIVED_AURORA_TICKET), "FLAG_RECEIVED_AURORA_TICKET is set") -- pokefirered/data/mystery_event_msg.s:271 vgoto_if_set FLAG_FOUGHT_LUGIA local fought = newSession() MysteryGift.receiveCard(fought, builtin("mystic_ticket").card) MysteryGift.setFlag(fought, 0x2F2, true) eq(MysteryGift.deliverGift(fought), MysteryGift.DELIVER_ALREADY, "a player who already fought LUGIA gets no second ticket") check(not Bag.has(fought.bag, MysteryGift.ITEM_MYSTIC_TICKET, 1), "and no ticket reaches the bag") print("[test] 5. An event mon carries the card's personality and OT") local eventCard = MysteryGift.normalizeCard(builtin("surf_pichu").card) eventCard.gift.personality = 0x12345678 eventCard.gift.otName = "GF" eventCard.gift.otId = 30518 local egg = newSession() MysteryGift.receiveCard(egg, eventCard) eq(MysteryGift.deliverGift(egg), MysteryGift.DELIVER_GIVEN, "the event egg is handed over") local mon = egg.party[#egg.party] local partyAfterGift = #egg.party check(mon ~= nil, "the event mon reached the party") eq(mon and mon.personality, 0x12345678, "the mon carries the card's personality") eq(mon and mon.otName, "GF", "the mon carries the card's OT name") eq(mon and mon.otId, 30518, "the mon carries the card's OT id") eq(mon and mon.species, 172, "the mon is the card's species") check(mon and mon.isEgg, "the Surfing Pichu card gives an egg") -- pokefirered/data/mystery_event_msg.s:72 setmonmetlocation METLOC_FATEFUL_ENCOUNTER eq(mon and mon.metLocation, 0xFF, "the mon is met at METLOC_FATEFUL_ENCOUNTER") check(mon and mon.modernFatefulEncounter, "the mon is a modern fateful encounter") -- pokefirered/data/mystery_event_msg.s:81 setmonmove slot, 2, MOVE_SURF eq(mon and mon.moves and mon.moves[3], 57, "the egg knows SURF in the third slot") -- pokefirered/data/mystery_event_msg.s:42 vgoto_if_unset FLAG_MYSTERY_GIFT_DONE eq(MysteryGift.deliverGift(egg), MysteryGift.DELIVER_ALREADY, "FLAG_MYSTERY_GIFT_DONE stops a second egg") eq(#egg.party, partyAfterGift, "the party did not grow a second time") local full = newSession() MysteryGift.receiveCard(full, eventCard) for _ = 1, 6 do full.party[#full.party + 1] = { species = 1, level = 5 } end eq(MysteryGift.deliverGift(full), MysteryGift.DELIVER_PARTY_FULL, "a full party is told to come back later") print("[test] 6. Stamp card and battle card stats") -- pokefirered/src/mystery_gift.c:307 MysteryGift_TrySaveStamp local stampSession = newSession() MysteryGift.receiveCard(stampSession, builtin("stamp_card").card) eq(MysteryGift.getCardStat(stampSession, MysteryGift.CARD_STAT_MAX_STAMPS), MysteryGift.MAX_STAMP_CARD_STAMPS, "the stamp card reports its maximum") check(MysteryGift.trySaveStamp(stampSession, { species = 25, id = 3 }), "a new stamp is accepted") check(not MysteryGift.trySaveStamp(stampSession, { species = 25, id = 3 }), "the same stamp is refused") check(not MysteryGift.trySaveStamp(stampSession, { species = 0, id = 4 }), "a speciesless stamp is refused") eq(MysteryGift.getCardStat(stampSession, MysteryGift.CARD_STAT_NUM_STAMPS), 1, "one stamp is counted") -- pokefirered/src/field_specials.c:1955 GetMysteryGiftCardStat eq(MysteryGift.getCardStatForScript(stampSession, MysteryGift.GET_NUM_STAMPS), 1, "GET_NUM_STAMPS reaches the same count") -- pokefirered/src/mystery_gift.c:561 MysteryGift_TryIncrementStat local battle = newSession() MysteryGift.receiveCard(battle, builtin("battle_card").card) MysteryGift.tryIncrementStat(battle, MysteryGift.CARD_STAT_BATTLES_WON, 111) eq(MysteryGift.getCardStat(battle, MysteryGift.CARD_STAT_BATTLES_WON), 0, "stats do not move until the card enables them") check(MysteryGift.tryEnableStatsByFlagId(battle, MysteryGift.getCardFlagId(battle)), "the battle card enables stats by its own flagId") MysteryGift.tryIncrementStat(battle, MysteryGift.CARD_STAT_BATTLES_WON, 111) MysteryGift.tryIncrementStat(battle, MysteryGift.CARD_STAT_BATTLES_WON, 111) eq(MysteryGift.getCardStat(battle, MysteryGift.CARD_STAT_BATTLES_WON), 1, "the same trainer only counts once") -- pokefirered/data/mystery_event_msg.s:166 three wins before the prize eq(MysteryGift.deliverGift(battle), MysteryGift.DELIVER_NOTHING, "the battle card gives no prize below three wins") MysteryGift.tryIncrementStat(battle, MysteryGift.CARD_STAT_BATTLES_WON, 222) MysteryGift.tryIncrementStat(battle, MysteryGift.CARD_STAT_BATTLES_WON, 333) eq(MysteryGift.getCardStat(battle, MysteryGift.CARD_STAT_BATTLES_WON), 3, "three trainers, three wins") eq(MysteryGift.deliverGift(battle), MysteryGift.DELIVER_GIVEN, "three wins pay the prize") MysteryGift.disableStats() print("[test] 7. Wonder News rewards") -- pokefirered/src/wonder_news.c:68 WonderNews_GetRewardInfo local newsSession = newSession() local newsRecord = { id = 3, sendType = MysteryGift.SEND_TYPE_ALLOWED, bgType = 0, titleText = "NEWS", bodyText = { "Hello." }, } check(MysteryGift.saveNews(newsSession, newsRecord), "the news saves") local rewardType, item = MysteryGift.getNewsRewardInfo(newsSession) eq(rewardType, MysteryGift.NEWS_REWARD_NONE, "no reward while Mystery Gift is disabled") MysteryGift.enable(newsSession) MysteryGift.setNewsReward(newsSession, MysteryGift.WONDER_NEWS_RECV_FRIEND) rewardType, item = MysteryGift.getNewsRewardInfo(newsSession) eq(rewardType, MysteryGift.NEWS_REWARD_RECV_SMALL, "news from a friend is a small reward") check(item ~= nil and item >= 133 and item <= 162, "the reward is a berry, got " .. tostring(item)) rewardType = MysteryGift.getNewsRewardInfo(newsSession) eq(rewardType, MysteryGift.NEWS_REWARD_WAITING, "the reward is spent after one visit") check(not MysteryGift.isNewsSameAsSaved(newsSession, { id = 4, titleText = "NEWS" }), "a different news record does not match the saved one") check(MysteryGift.isNewsSameAsSaved(newsSession, newsRecord), "the same news record matches") print("[test] 8. Feed cards hand their gifts over") local feed = MysteryGift.parseFeed(table.concat({ '{"cards":[', '{"bgType":1,"bodyText":["Line one.","Line two.","",""],"flagId":1003,', '"footerLine1Text":"","footerLine2Text":"","gift":{"item":13,"kind":"item","quantity":2,', '"setFlags":[2122]},"iconSpecies":25,"idNumber":30,"key":"potions","maxStamps":0,', '"sendType":0,"subtitleText":"FROM THE CLUB","titleText":"SURPRISE","type":0},', '{"bgType":7,"bodyText":["","","",""],"flagId":1008,"footerLine1Text":"","footerLine2Text":"",', '"gift":{"heldItem":139,"kind":"mon","level":10,"moves":{"1":1},"otId":20078,"otName":"AURA",', '"species":151},"iconSpecies":151,"idNumber":31,"key":"mew","maxStamps":0,"sendType":0,', '"subtitleText":"","titleText":"MEW","type":0}', '],"issued":5,"news":[],"v":1}', })) check(feed ~= nil and #feed.cards == 2, "the feed payload parses into two cards") local potions = feed and feed.cards[1].card eq(potions and potions.titleText, "SURPRISE", "the feed card keeps its title") eq(potions and potions.bodyText[2], "Line two.", "the feed card keeps its body lines") eq(potions and potions.gift.setFlags[1], 0x84A, "the feed card keeps its gift flag") local fed = newSession() check(MysteryGift.receiveCard(fed, potions), "the feed card saves") eq(MysteryGift.deliverGift(fed), MysteryGift.DELIVER_GIVEN, "the feed card hands its item over") eq(Bag.get(fed.bag, 13), 2, "both POTIONs arrived") local mewCard = feed and feed.cards[2].card eq(mewCard and mewCard.gift.heldItem, 139, "the mon gift keeps its held item") local mewSession = newSession() check(MysteryGift.receiveCard(mewSession, mewCard), "the MEW card saves") check(MysteryGift.validateSavedCard(mewSession), "the held item is inside the card checksum") eq(MysteryGift.deliverGift(mewSession), MysteryGift.DELIVER_GIVEN, "the MEW card hands the mon over") local mew = mewSession.party[#mewSession.party] eq(mew and mew.species, 151, "MEW joined the party") eq(mew and mew.heldItem, 139, "holding the item from the card") eq(mew and mew.otName, "AURA", "with the event OT") MysteryGift.getSavedCard(mewSession).gift.heldItem = 1 check(not MysteryGift.validateSavedCard(mewSession), "a changed held item breaks the checksum") print("[test] 9. The script specials") local fakeRuntime = { getSession = function() return nil end } package.loaded["src.core.game3.runtime"] = fakeRuntime local Natives = require("src.core.game3.scripting.natives") local specials = require("src.core.game3.scripting.natives_gift").SPECIAL_IDS local scriptSession = newSession() fakeRuntime.getSession = function() return scriptSession end local ctx = { specialVars = {}, stringVars = {} } local _, value, known = Natives.special(ctx, specials.ValidateSavedWonderCard, {}) check(known, "ValidateSavedWonderCard is a known special") eq(value, 0, "no card saved reads as FALSE") MysteryGift.receiveCard(scriptSession, builtin("stamp_card").card) MysteryGift.trySaveStamp(scriptSession, { species = 25, id = 3 }) _, value = Natives.special(ctx, specials.ValidateSavedWonderCard, {}) eq(value, 1, "a saved card reads as TRUE") ctx.specialVars[0x800D] = MysteryGift.GET_NUM_STAMPS _, value = Natives.special(ctx, specials.GetMysteryGiftCardStat, {}) eq(value, 1, "GetMysteryGiftCardStat returns the stamp count") ctx.specialVars[0x800D] = MysteryGift.GET_MAX_STAMPS _, value = Natives.special(ctx, specials.GetMysteryGiftCardStat, {}) eq(value, MysteryGift.MAX_STAMP_CARD_STAMPS, "GetMysteryGiftCardStat returns the stamp maximum") MysteryGift.enable(scriptSession) MysteryGift.saveNews(scriptSession, newsRecord) MysteryGift.setNewsReward(scriptSession, MysteryGift.WONDER_NEWS_RECV_WIRELESS) _, value = Natives.special(ctx, specials.WonderNews_GetRewardInfo, {}) eq(value, MysteryGift.NEWS_REWARD_RECV_BIG, "WonderNews_GetRewardInfo returns the reward type") check((ctx.specialVars[0x800D] or 0) >= 133, "and leaves the berry in VAR_RESULT, got " .. tostring(ctx.specialVars[0x800D])) print("[test] 10. ClearMysteryGift") -- pokefirered/src/mystery_gift.c:55 MysteryGift.clear(scriptSession) check(not MysteryGift.validateSavedCard(scriptSession), "the card is gone") check(not MysteryGift.validateSavedNews(scriptSession), "the news is gone") eq(MysteryGift.getCardStat(scriptSession, MysteryGift.CARD_STAT_NUM_STAMPS), 0, "the stamps are gone") if failed == 0 then print("PASS game3_gift_model") os.exit(0) else print("FAIL game3_gift_model failures=" .. failed) os.exit(1) end