#!/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 local function finish() if failed > 0 then print("[test] FAILED " .. failed) os.exit(1) end print("[test] all passed") os.exit(0) end package.loaded["src.core.game3.audio"] = setmetatable({}, { __index = function() return function() end end, }) local store = { flags = {}, vars = {} } local session = { store = store, map = "FR_ROUTE_2_HOUSE", party = {}, name = "RED", trainerId = 4242 } package.loaded["src.core.game3.runtime"] = { getSession = function() return session end, isActive = function() return true end, } local Std = require("src.core.game3.scripting.stdscripts") local Natives = require("src.core.game3.scripting.natives") local Flags = require("src.core.game3.scripting.flags") local okTrade, Trade = pcall(require, "src.core.game3.scripting.natives_trade") local okDaycare, Daycare = pcall(require, "src.core.game3.scripting.natives_daycare") check(okTrade, "src/core/game3/scripting/natives_trade.lua loads") check(okDaycare, "src/core/game3/scripting/natives_daycare.lua loads") if not (okTrade and okDaycare) then finish() end local VAR_RESULT = 0x800D local VAR_0x8004 = 0x8004 local VAR_0x8005 = 0x8005 local function newCtx() return { specialVars = {}, stringVars = { [1] = "", [2] = "", [3] = "" } } end local function getVar(ctx, id) return tonumber(Flags.getVar(store, ctx, id)) or 0 end local function setVar(ctx, id, v) Flags.setVar(store, ctx, id, v) end print("[test] 1. the four trade ids and the sixteen daycare ids are bound") local IDS = { GetInGameTradeSpeciesInfo = 0xFC, CreateInGameTradePokemon = 0xFD, DoInGameTradeScene = 0xFE, GetTradeSpecies = 0xFF, GetDaycareMonNicknames = 0xB5, RejectEggFromDayCare = 0xB7, GiveEggFromDaycare = 0xB8, SetDaycareCompatibilityString = 0xB9, StoreSelectedPokemonInDaycare = 0xBB, ChooseSendDaycareMon = 0xBC, ShowDaycareLevelMenu = 0xBD, GetNumLevelsGainedFromDaycare = 0xBE, GetDaycareCost = 0xBF, TakePokemonFromDaycare = 0xC0, GetDaycarePokemonCount = 0x15F, PutMonInRoute5Daycare = 0x176, GetCostToWithdrawRoute5DaycareMon = 0x177, IsThereMonInRoute5Daycare = 0x178, GetNumLevelsGainedForRoute5DaycareMon = 0x179, TakePokemonFromRoute5Daycare = 0x17A, } for name, id in pairs(IDS) do eq(Std.SPECIAL[name], id, "Std.SPECIAL." .. name) check(Natives.ALLOW["special:" .. id] ~= nil, name .. " is bound in Natives.ALLOW") end local moduleNames = {} for _, n in ipairs(Natives.MODULE_NAMES) do moduleNames[n] = true end check(moduleNames["natives_trade"], "discovery found natives_trade with no natives.lua edit") check(moduleNames["natives_daycare"], "discovery found natives_daycare with no natives.lua edit") local Cache = require("tests.game3_cache") local cacheRoot = Cache.mount("meta.json") if not cacheRoot then print("[skip] game3_special_trade_test needs species data: " .. tostring(Cache.reason)) finish() end print("[info] FireRed cache at " .. cacheRoot) print("[test] 2. the in-game trade table matches pokefirered/src/data/ingame_trades.h") eq(Trade.COUNT, 9, "nine in-game trades") local mimien = Trade.TRADES[0] eq(mimien.nickname, "MIMIEN", "INGAME_TRADE_MR_MIME nickname") eq(mimien.species, 122, "INGAME_TRADE_MR_MIME offers MR. MIME") eq(mimien.requestedSpecies, 63, "INGAME_TRADE_MR_MIME wants ABRA") eq(mimien.otId, 1985, "INGAME_TRADE_MR_MIME otId") eq(mimien.personality, 0x00009cae, "INGAME_TRADE_MR_MIME personality") eq(Trade.TRADES[1].heldItem, 131, "ZYNX holds ITEM_FAB_MAIL") eq(Trade.TRADES[2].species, 29, "the FireRed Nidoran trade offers NIDORAN_F") eq(Trade.TRADES[2].requestedSpecies, 32, "and wants NIDORAN_M") eq(Trade.TRADES[5].requestedSpecies, 55, "the FireRed Lickitung trade wants GOLDUCK") eq(Trade.TRADES[6].abilityNum, 1, "ESPHERE uses the second ability slot") eq(Trade.TRADES[8].nickname, "SEELOR", "INGAME_TRADE_SEEL nickname") local Pokemon = require("src.core.game3.pokemon") Pokemon.install(nil) local Experience = require("src.core.game3.battle.experience") local SummaryData = require("src.core.game3.summary_data") local function makeMon(species, level, extra) local Party = require("src.core.game3.party") local scratch = { party = {}, name = "RED", trainerId = 4242 } local ok, _, mon = Party.giveMon(scratch, species, level, "") check(ok, "built a level " .. level .. " species " .. species .. " for the test") for k, v in pairs(extra or {}) do mon[k] = v end return mon end print("[test] 3. GetInGameTradeSpeciesInfo buffers both names and returns the wanted species") local ctx = newCtx() setVar(ctx, VAR_0x8004, 0) local _, requested = Natives.special(ctx, Std.SPECIAL.GetInGameTradeSpeciesInfo, nil) eq(requested, 63, "VAR_RESULT is SPECIES_ABRA") eq(ctx.stringVars[1], Pokemon.name(63), "STR_VAR_1 is the requested species name") eq(ctx.stringVars[2], Pokemon.name(122), "STR_VAR_2 is the offered species name") print("[test] 4. GetTradeSpecies reads the chosen party slot and rejects eggs") session.party = { makeMon(63, 18), makeMon(25, 12) } ctx = newCtx() setVar(ctx, VAR_0x8005, 0) local _, species = Natives.special(ctx, Std.SPECIAL.GetTradeSpecies, nil) eq(species, 63, "slot 0 is the ABRA the trade wants") setVar(ctx, VAR_0x8005, 1) _, species = Natives.special(ctx, Std.SPECIAL.GetTradeSpecies, nil) eq(species, 25, "slot 1 is the PIKACHU") session.party[2].isEgg = true _, species = Natives.special(ctx, Std.SPECIAL.GetTradeSpecies, nil) eq(species, 0, "an egg reads back as SPECIES_NONE") session.party[2].isEgg = nil print("[test] 5. CreateInGameTradePokemon builds the cart's mon at the sent mon's level") ctx = newCtx() setVar(ctx, VAR_0x8004, 0) setVar(ctx, VAR_0x8005, 0) Natives.special(ctx, Std.SPECIAL.CreateInGameTradePokemon, nil) local offered = Trade._offered check(offered ~= nil, "the offered mon exists") eq(offered.species, 122, "the offered mon is MR. MIME") eq(offered.level, 18, "it arrives at the level of the mon being sent") eq(offered.nickname, "MIMIEN", "it carries the cart nickname") eq(offered.otId, 1985, "it carries the cart otId") eq(offered.otName, "REYLEY", "it carries the cart OT name") eq(offered.personality, 0x00009cae, "it carries the cart personality") eq(offered.ivs.hp, 20, "IV hp from the table") eq(offered.ivs.spd, 22, "IV spDef from the table") eq(offered.metLocation, 0xFE, "met location is METLOC_IN_GAME_TRADE") local expected = Pokemon.calcStats(122, 18, offered.ivs, offered.evs, offered.personality) eq(offered.maxHp, expected.maxHp, "stats recalculated from the fixed IVs") print("[test] 6. DoInGameTradeScene swaps the mon after the cart's frames") local tradeTargetCalls = {} local Evolution = require("src.core.game3.evolution") local realTradeTarget = Evolution.tradeTarget Evolution.tradeTarget = function(mon, sess) tradeTargetCalls[#tradeTargetCalls + 1] = mon return realTradeTarget(mon, sess) end ctx = newCtx() setVar(ctx, VAR_0x8004, 0) setVar(ctx, VAR_0x8005, 0) Natives.special(ctx, Std.SPECIAL.CreateInGameTradePokemon, nil) Natives.special(ctx, Std.SPECIAL.DoInGameTradeScene, nil) local task = ctx.stateWait check(type(task) == "function", "DoInGameTradeScene armed a waitstate task") eq(session.party[1].species, 63, "the ABRA is still in the party while the fade runs") local frames = 0 while frames < 2000 and not task() do frames = frames + 1 end check(frames > 60, "the scene held for at least the cart's fade plus hold, held " .. frames) eq(session.party[1].species, 122, "the party slot now holds MR. MIME") eq(session.party[1].friendship, 70, "a traded mon starts at 70 friendship") check(session.dex and session.dex.owned and session.dex.owned[122], "the received species is registered as owned") eq(#tradeTargetCalls, 1, "the trade evolution gate ran once") eq(tradeTargetCalls[1], session.party[1], "it ran on the mon that landed in the party") eq(ctx.stringVars[1], "MIMIEN", "STR_VAR_1 is the received mon's nickname") print("[test] 7. the same gate evolves a mon that does trade-evolve") Evolution.tradeTarget = realTradeTarget local kadabra = makeMon(64, 20) session.dex = session.dex or { seen = {}, owned = {} } session.dex.national = true local target = Evolution.tradeTarget(kadabra, session) eq(target, 65, "KADABRA's trade target is ALAKAZAM") Trade.tryTradeEvolution(kadabra, session) eq(kadabra.species, 65, "Trade.tryTradeEvolution applied it headless") print("[test] 8. the Route 5 daycare and the Four Island daycare are separate stores") session.party = { makeMon(19, 5), makeMon(16, 8) } session.modData = nil session.daycare = nil session.route5Daycare = nil ctx = newCtx() setVar(ctx, VAR_0x8004, 0) Natives.special(ctx, Std.SPECIAL.PutMonInRoute5Daycare, nil) eq(#session.party, 1, "the party lost the deposited mon") eq(session.party[1].species, 16, "and compacted") local _, there = Natives.special(newCtx(), Std.SPECIAL.IsThereMonInRoute5Daycare, nil) eq(there, 1, "IsThereMonInRoute5Daycare is TRUE") local _, fourIsland = Natives.special(newCtx(), Std.SPECIAL.GetDaycarePokemonCount, nil) eq(fourIsland, 0, "the Four Island daycare is still empty") local _, state = Natives.special(newCtx(), Std.SPECIAL.GetDaycareState, nil) eq(state, 0, "GetDaycareState is DAYCARE_NO_MONS") print("[test] 9. levels gained and the cost follow pret's 100 + 100 per level") local r5 = Daycare.route5Of(session) check(r5.mon ~= nil, "the Route 5 slot holds the mon") eq(r5.mon.level, 5, "deposited at level 5") local growth = Experience.growthRate(r5.mon) local stepsFor3 = SummaryData.expForLevel(growth, 8) - SummaryData.expForLevel(growth, 5) r5.steps = stepsFor3 ctx = newCtx() local _, gained = Natives.special(ctx, Std.SPECIAL.GetNumLevelsGainedForRoute5DaycareMon, nil) eq(gained, 3, "three levels gained from the accumulated steps") eq(ctx.stringVars[2], "3", "STR_VAR_2 carries the level count") ctx = newCtx() Natives.special(ctx, Std.SPECIAL.GetCostToWithdrawRoute5DaycareMon, nil) eq(getVar(ctx, VAR_0x8005), 400, "the cost is 100 + 100 * 3") eq(Daycare.cost(r5.mon, 0), 100, "no levels gained costs the flat 100") print("[test] 10. taking the Route 5 mon back levels it up and returns the species") ctx = newCtx() local _, taken = Natives.special(ctx, Std.SPECIAL.TakePokemonFromRoute5Daycare, nil) eq(taken, 19, "TakePokemonFromRoute5Daycare returns the species") eq(#session.party, 2, "the mon is back in the party") eq(session.party[2].level, 8, "it came back three levels higher") eq(session.party[2].hp, session.party[2].maxHp, "and at full HP, as BoxMonToMon leaves it") local _, stillThere = Natives.special(newCtx(), Std.SPECIAL.IsThereMonInRoute5Daycare, nil) eq(stillThere, 0, "the Route 5 slot is empty again") print("[test] 11. the Four Island daycare holds two and shifts on withdrawal") session.party = { makeMon(19, 5), makeMon(16, 8), makeMon(10, 3) } session.modData = nil session.daycare = nil ctx = newCtx() setVar(ctx, VAR_0x8004, 0) Natives.special(ctx, Std.SPECIAL.StoreSelectedPokemonInDaycare, nil) ctx = newCtx() setVar(ctx, VAR_0x8004, 0) Natives.special(ctx, Std.SPECIAL.StoreSelectedPokemonInDaycare, nil) local _, count = Natives.special(newCtx(), Std.SPECIAL.GetDaycarePokemonCount, nil) eq(count, 2, "two mons in the Four Island daycare") local _, twoState = Natives.special(newCtx(), Std.SPECIAL.GetDaycareState, nil) eq(twoState, 3, "GetDaycareState is DAYCARE_TWO_MONS") eq(#session.party, 1, "only the third mon is left in the party") ctx = newCtx() Natives.special(ctx, Std.SPECIAL.GetDaycareMonNicknames, nil) eq(ctx.stringVars[1], Pokemon.name(19), "STR_VAR_1 is the first daycare mon") eq(ctx.stringVars[2], Pokemon.name(16), "STR_VAR_2 is the second daycare mon") eq(ctx.stringVars[3], "RED", "STR_VAR_3 is the first mon's OT name") ctx = newCtx() setVar(ctx, VAR_0x8004, 0) local _, first = Natives.special(ctx, Std.SPECIAL.TakePokemonFromDaycare, nil) eq(first, 19, "the first slot came back") local dc = Daycare.stateOf(session) eq(dc[1] and dc[1].species, 16, "slot 2 shifted down into slot 1") check(dc[2] == nil, "slot 2 is empty") local _, oneState = Natives.special(newCtx(), Std.SPECIAL.GetDaycareState, nil) eq(oneState, 2, "GetDaycareState is DAYCARE_ONE_MON") print("[test] 12. the level menu rows carry the nickname and the level after steps") dc = Daycare.stateOf(session) dc.steps[1] = SummaryData.expForLevel(Experience.growthRate(dc[1]), 11) - SummaryData.expForLevel(Experience.growthRate(dc[1]), 8) local rows = Daycare.levelMenuRows(dc) eq(#rows, 3, "three rows, two mons plus EXIT") eq(rows[1].text, Pokemon.name(16), "row 1 names the deposited mon") eq(rows[1].tail, "Lv11", "row 1 shows the level the steps have earned") eq(rows[3].text, "EXIT", "row 3 is EXIT") eq(Daycare.LEVEL_MENU_LAYOUT.left, 12, "the window sits at tilemapLeft 12") eq(Daycare.LEVEL_MENU_LAYOUT.width, 17, "and is 17 tiles wide") print("[test] 13. the compatibility string picks pret's four lines") local function pair(a, b, idA, idB) local left = makeMon(a, 20, { otId = idA }) local right = makeMon(b, 20, { otId = idB }) session.daycare = { left, right, steps = { 0, 0 } } return Daycare.compatibility(session.daycare) end eq(pair(132, 132, 1, 2), Daycare.PARENTS_INCOMPATIBLE, "two DITTO cannot breed") eq(pair(144, 145, 1, 2), Daycare.PARENTS_INCOMPATIBLE, "two undiscovered-group legendaries cannot breed") local sameSpeciesDifferentTrainer = pair(19, 19, 1, 2) check(sameSpeciesDifferentTrainer == Daycare.PARENTS_MAX_COMPATIBILITY or sameSpeciesDifferentTrainer == Daycare.PARENTS_INCOMPATIBLE, "same species, different trainers is MAX unless the genders collide, got " .. tostring(sameSpeciesDifferentTrainer)) eq(Daycare.compatibilityText(Daycare.PARENTS_MAX_COMPATIBILITY), "The two seem to get along\nvery well.", "the MAX line") eq(Daycare.compatibilityText(Daycare.PARENTS_INCOMPATIBLE), "The two prefer to play with other\nPOKéMON than each other.", "the incompatible line") ctx = newCtx() session.daycare = { makeMon(132, 20, { otId = 1 }), makeMon(132, 20, { otId = 2 }), steps = { 0, 0 } } Natives.special(ctx, Std.SPECIAL.SetDaycareCompatibilityString, nil) eq(ctx.stringVars[4], "The two prefer to play with other\nPOKéMON than each other.", "SetDaycareCompatibilityString writes gStringVar4") print("[test] 14. ShowFieldMessageStringVar4 puts gStringVar4 on screen") local shown = nil local adapters = { openMessageStay = function(text) shown = text end } Natives.special(ctx, Std.SPECIAL.ShowFieldMessageStringVar4, adapters) eq(shown, ctx.stringVars[4], "the compatibility line reached the message box") check(ctx.messageOpen == true, "the field message box is open") print("[test] 15. GiveEggFromDaycare hands the egg over and RejectEggFromDayCare clears it") session.party = { makeMon(19, 5) } session.daycare = { makeMon(132, 20), makeMon(19, 20), steps = { 0, 0 }, eggPending = true } local _, eggState = Natives.special(newCtx(), Std.SPECIAL.GetDaycareState, nil) eq(eggState, 1, "GetDaycareState is DAYCARE_EGG_WAITING") -- pokefirered/src/daycare.c:1133 Natives.special(newCtx(), Std.SPECIAL.GiveEggFromDaycare, nil) local partyCount, eggCount = 0, 0 for i = 1, 6 do local mon = session.party[i] if mon then partyCount = partyCount + 1 if mon.isEgg then eggCount = eggCount + 1 end end end eq(partyCount, 2, "the egg joined the party") eq(eggCount, 1, "and it is an egg") local _, afterGive = Natives.special(newCtx(), Std.SPECIAL.GetDaycareState, nil) eq(afterGive, 3, "the pending egg is cleared, back to DAYCARE_TWO_MONS") session.daycare.offspringPersonality = 0x1234 local _, pendingAgain = Natives.special(newCtx(), Std.SPECIAL.GetDaycareState, nil) eq(pendingAgain, 1, "a non-zero offspringPersonality is an egg waiting") Natives.special(newCtx(), Std.SPECIAL.RejectEggFromDayCare, nil) eq(session.daycare.offspringPersonality, 0, "RejectEggFromDayCare zeroes offspringPersonality") local _, afterReject = Natives.special(newCtx(), Std.SPECIAL.GetDaycareState, nil) eq(afterReject, 3, "and the daycare is back to DAYCARE_TWO_MONS") print("[test] 16. both daycare stores survive a save round trip") local Schema = require("src.core.game3.save_schema_firered") local SaveData = require("src.core.SaveData") session = Schema.newGame({}) session.store = store session.party = { makeMon(19, 5), makeMon(16, 9) } ctx = newCtx() setVar(ctx, VAR_0x8004, 0) Natives.special(ctx, Std.SPECIAL.PutMonInRoute5Daycare, nil) ctx = newCtx() setVar(ctx, VAR_0x8004, 0) Natives.special(ctx, Std.SPECIAL.StoreSelectedPokemonInDaycare, nil) eq(#session.party, 0, "both mons left the party") local reloaded = Schema.fromSaveTable(SaveData.decode(SaveData.encode(Schema.toSaveTable(session)))) reloaded.store = store session = reloaded local _, thereAfter = Natives.special(newCtx(), Std.SPECIAL.IsThereMonInRoute5Daycare, nil) eq(thereAfter, 1, "the Route 5 mon is still there after encode / decode") local _, stateAfter = Natives.special(newCtx(), Std.SPECIAL.GetDaycareState, nil) eq(stateAfter, 2, "the Four Island mon is still there, DAYCARE_ONE_MON") eq(Daycare.route5Of(session).mon.species, 19, "the Route 5 mon kept its species") eq(Daycare.stateOf(session)[1].species, 16, "the Four Island mon kept its species") eq(#session.party, 0, "and neither came back into the party") ctx = newCtx() setVar(ctx, VAR_0x8004, 0) local _, backAfterLoad = Natives.special(ctx, Std.SPECIAL.TakePokemonFromRoute5Daycare, nil) eq(backAfterLoad, 19, "the reloaded mon can still be withdrawn") eq(#session.party, 1, "and lands back in the party") print("[test] 17. the party picker gives the held screen back") local Fade = require("src.ui.game3.fade") local restores = 0 local realBegin = Fade.begin Fade.begin = function(mode) restores = restores + 1; Fade.mode = mode end Fade.active, Fade.t, Fade.mode = false, 16, Fade.MODE.TO_BLACK local choose = { chooseParty = function(_, done) done(0) end } Natives.choosePartyMon(newCtx(), choose, 6) eq(Fade.t, 0, "the held black is cleared before the party menu draws") eq(restores, 1, "and a fade is begun again when the picker closes") eq(Fade.mode, Fade.MODE.FROM_BLACK, "the restore is FadeInFromBlack") Fade.active, Fade.t, Fade.mode, restores = false, 16, Fade.MODE.TO_WHITE, 0 Natives.choosePartyMon(newCtx(), choose, 6) eq(Fade.t, 16, "a held white screen is left alone") eq(restores, 0, "and nothing fades in from black over it") Fade.active, Fade.t, Fade.mode, restores = false, 0, Fade.MODE.FROM_BLACK, 0 Natives.choosePartyMon(newCtx(), choose, 6) eq(Fade.t, 0, "an uncovered screen is left alone") eq(restores, 0, "and gets no restore") Fade.begin = realBegin print("[test] 18. no handler in either module reaches the unknown-special log") Natives.resetLog() local logs = {} local quiet = { log = function(m) logs[#logs + 1] = m end } for _, mod in ipairs({ Trade, Daycare }) do for id in pairs(mod.HANDLERS) do local _, _, known = Natives.special(newCtx(), id, quiet) check(known, string.format("special 0x%X dispatches to a handler", id)) end end local ListMenu = require("src.core.game3.scripting.natives_listmenu") ListMenu.Menu.close() eq(#logs, 0, "nothing reached the unknown-special log") finish()