Files
gen1recomp/tests/game3_stitchscript_mail_test.lua
bryanthaboi dee935a4ea CLOSES #2430
2026-09-23 11:58:32 -04:00

311 lines
13 KiB
Lua

#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
require("tests.game3_cache").stubSpeciesNames()
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,
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,
}
local GameVersion = require("src.core.GameVersion")
GameVersion.set("firered")
local Flags = require("src.core.game3.scripting.flags")
local Vm = require("src.core.game3.scripting.vm")
local Adapters = require("src.core.game3.scripting.adapters")
local Runtime = require("src.core.game3.runtime")
local Mail = require("src.core.game3.mail")
local Trade = require("src.core.game3.scripting.natives_trade")
local Daycare = require("src.core.game3.scripting.natives_daycare")
local Party = require("src.core.game3.party")
local Schema = require("src.core.game3.save_schema_firered")
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 VAR_0x8004 = 0x8004
local VAR_0x8005 = 0x8005
local SPECIAL_CREATE_IN_GAME_TRADE_POKEMON = 0xFD
local SPECIAL_DO_IN_GAME_TRADE_SCENE = 0xFE
local SPECIAL_PUT_MON_IN_ROUTE5_DAYCARE = 0x176
local SPECIAL_TAKE_POKEMON_FROM_ROUTE5_DAYCARE = 0x17A
local SPECIAL_STORE_SELECTED_POKEMON_IN_DAYCARE = 0xBB
local SPECIAL_TAKE_POKEMON_FROM_DAYCARE = 0xC0
local SPECIES_POLIWHIRL = 61
local SPECIES_JYNX = 124
local SPECIES_ABRA = 63
local ITEM_FAB_MAIL = 131
local ITEM_ORANGE_MAIL = 121
local ITEM_POTION = 13
-- pokefirered/src/data/ingame_trades.h:184 sInGameTradeMailMessages
local ZYNX_WORDS = { 3613, 4128, 5147, 10876, 3072, 4102, 5183, 4143, 4137 }
local prevSession = Runtime.session
local function newSession()
local session = {
party = {},
store = Flags.newStore(),
dex = { seen = {}, owned = {} },
modData = {},
name = "RED",
trainerId = 24680,
}
Runtime.session = session
return session
end
local function newVm(rows)
local vm = Vm.new({
store = Flags.newStore(),
scripts = { t = rows },
adapters = Adapters.host(nil, nil, nil),
})
vm:start("t")
return vm
end
local function runToEnd(rows, maxFrames)
local vm = newVm(rows)
for _ = 1, maxFrames or 2000 do
if not vm:isRunning() then break end
vm:resume()
end
return vm
end
local function wordsOf(record)
local out = {}
for i = 1, Mail.MAIL_WORDS_COUNT do out[i] = record and record.words and record.words[i] end
return table.concat(out, ",")
end
print("[test] 1. ItemIsMail is the twelve mail items and nothing else")
local mailItems = { 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132 }
local allMail = true
for _, id in ipairs(mailItems) do
if not Mail.isMailItem(id) then allMail = false end
end
check(allMail, "all twelve of pret's ITEM_ORANGE_MAIL..ITEM_RETRO_MAIL are mail")
check(not Mail.isMailItem(120) and not Mail.isMailItem(133) and not Mail.isMailItem(ITEM_POTION),
"the items on either side of the block, and POTION, are not")
eq(Mail.designOf(ITEM_ORANGE_MAIL), 0, "ITEM_TO_MAIL(ORANGE_MAIL) is design 0")
eq(Mail.designOf(ITEM_FAB_MAIL), 10, "ITEM_TO_MAIL(FAB_MAIL) is design 10")
eq(Mail.designOf(ITEM_POTION), nil, "a non mail item has no design")
print("[test] 2. GiveMailToMon only uses the six party-half slots, TakeMailFromMon frees one")
local s2 = newSession()
local held = {}
for i = 1, 7 do
local mon = { species = SPECIES_ABRA, level = 5, personality = i }
held[i] = mon
held[i].id = Mail.giveMailToMon(s2, mon, ITEM_ORANGE_MAIL)
end
eq(held[1].id, 0, "the first letter takes mail slot 0")
eq(held[6].id, 5, "the sixth takes mail slot 5, the last of the party half")
eq(held[7].id, Mail.MAIL_NONE, "the seventh is refused with MAIL_NONE")
eq(held[7].item, nil, "and the refused mon was never given the item")
check(Mail.monHasMail(held[1]), "MonHasMail is true for a mon holding its letter")
eq(held[1].item, ITEM_ORANGE_MAIL, "GiveMailToMon set the held item too")
eq(Mail.get(s2, 0).playerName, "RED", "the author is the player name")
eq(Mail.get(s2, 0).trainerId, 24680, "and the player trainer id")
eq(Mail.get(s2, 0).species, SPECIES_ABRA, "SpeciesToMailSpecies stamped the mon species")
eq(Mail.get(s2, 0).words[1], Mail.EC_WORD_UNDEFINED, "a blank letter is nine empty words")
Mail.takeMailFromMon(s2, held[1])
check(not Mail.monHasMail(held[1]), "TakeMailFromMon leaves the mon with no mail")
eq(held[1].item, 0, "and no held item")
eq(Mail.get(s2, 0), nil, "the pool slot is free again")
local reuse = { species = SPECIES_ABRA, level = 5, personality = 99 }
eq(Mail.giveMailToMon(s2, reuse, ITEM_FAB_MAIL), 0, "and the next letter reuses slot 0")
check(not Mail.monHasMail({ species = 1, item = ITEM_FAB_MAIL }),
"a mon holding mail with no mail id does not have mail")
if not require("tests.game3_cache").mount() then
print("[skip] 3-4. the in-game trades are ROM data: " .. tostring(require("tests.game3_cache").reason))
else
print("[test] 3. the ZYNX trade carries DONTAE's FAB MAIL, run through the real specials")
local s3 = newSession()
check(select(1, Party.giveMon(s3, SPECIES_POLIWHIRL, 20, "POLI")) == true,
"the player has the POLIWHIRL DONTAE asks for")
local vm3 = runToEnd({
{ op = "setvar", [1] = VAR_0x8004, [2] = 1 },
{ op = "setvar", [1] = VAR_0x8005, [2] = 0 },
{ op = "special", [1] = SPECIAL_CREATE_IN_GAME_TRADE_POKEMON,
id = SPECIAL_CREATE_IN_GAME_TRADE_POKEMON },
{ op = "special", [1] = SPECIAL_DO_IN_GAME_TRADE_SCENE,
id = SPECIAL_DO_IN_GAME_TRADE_SCENE },
{ op = "waitstate" },
{ op = "end" },
})
check(not vm3:isRunning(), "the trade scene finished")
local zynx = s3.party[1]
eq(zynx and (zynx.species or zynx.speciesId), SPECIES_JYNX, "ZYNX is in the party slot")
eq(zynx and zynx.item, ITEM_FAB_MAIL, "holding FAB MAIL")
check(Mail.monHasMail(zynx), "and MonHasMail says the letter came with it")
local letter = Mail.get(s3, zynx and zynx.mail)
check(letter ~= nil, "the letter is in the player's mail pool")
eq(wordsOf(letter), table.concat(ZYNX_WORDS, ","),
"with sInGameTradeMailMessages[0], the nine easy chat words")
eq(letter and letter.playerName, "DONTAE", "signed by DONTAE, not by the player")
eq(letter and letter.trainerId, 36728, "with DONTAE's trainer id")
eq(letter and letter.itemId, ITEM_FAB_MAIL, "and the FAB MAIL stationery")
eq(letter and letter.design, 10, "which resolves to mail design 10")
print("[test] 4. the mail of the mon the player sends away is freed")
local s4 = newSession()
Party.giveMon(s4, SPECIES_POLIWHIRL, 20, "POLI")
eq(Mail.giveMailToMon(s4, s4.party[1], ITEM_ORANGE_MAIL), 0,
"the POLIWHIRL leaves carrying its own ORANGE MAIL in slot 0")
local before = Mail.get(s4, 0)
eq(before and before.playerName, "RED", "which the player wrote")
runToEnd({
{ op = "setvar", [1] = VAR_0x8004, [2] = 1 },
{ op = "setvar", [1] = VAR_0x8005, [2] = 0 },
{ op = "special", [1] = SPECIAL_CREATE_IN_GAME_TRADE_POKEMON,
id = SPECIAL_CREATE_IN_GAME_TRADE_POKEMON },
{ op = "special", [1] = SPECIAL_DO_IN_GAME_TRADE_SCENE,
id = SPECIAL_DO_IN_GAME_TRADE_SCENE },
{ op = "waitstate" },
{ op = "end" },
})
local after = Mail.get(s4, 0)
eq(after and after.playerName, "DONTAE",
"ClearMailStruct freed slot 0 and GiveMailToMon2 reused it for the partner letter")
eq(s4.party[1] and s4.party[1].mail, 0, "the received ZYNX points at that slot")
local occupied = 0
for i = 1, Mail.MAIL_COUNT do
if not Mail.isEmpty(Mail.pool(s4)[i]) then occupied = occupied + 1 end
end
eq(occupied, 1, "exactly one letter is in the pool, the sent one did not leak")
end
print("[test] 5. the Route 5 day care moves the letter into its slot and hands it back")
local s5 = newSession()
Party.giveMon(s5, SPECIES_POLIWHIRL, 20, "POLI")
Party.giveMon(s5, SPECIES_ABRA, 20, "ABBY")
Mail.giveMailToMon(s5, s5.party[1], ITEM_FAB_MAIL)
for i = 1, Mail.MAIL_WORDS_COUNT do Mail.get(s5, 0).words[i] = ZYNX_WORDS[i] end
local depositedWords = wordsOf(Mail.get(s5, 0))
runToEnd({
{ op = "setvar", [1] = VAR_0x8004, [2] = 0 },
{ op = "special", [1] = SPECIAL_PUT_MON_IN_ROUTE5_DAYCARE,
id = SPECIAL_PUT_MON_IN_ROUTE5_DAYCARE },
{ op = "end" },
})
local r5 = Daycare.route5Of(s5)
check(r5 and r5.mon ~= nil, "the POLIWHIRL is in the Route 5 day care")
eq(r5 and r5.mon and r5.mon.item, 0, "TakeMailFromMon took its held mail on the way in")
eq(r5 and r5.mon and r5.mon.mail, nil, "and its mail id")
check(r5 and r5.mail ~= nil, "the letter moved into the day-care slot")
eq(r5 and r5.mail and r5.mail.otName, "RED", "DayCareMail.OT_name is the player")
eq(r5 and r5.mail and r5.mail.monName, "POLI", "DayCareMail.monName is the mon nickname")
eq(Mail.get(s5, 0), nil, "and the pool slot it used is free while the mon is away")
eq(#s5.party, 1, "the party compacted down to the ABRA")
runToEnd({
{ op = "special", [1] = SPECIAL_TAKE_POKEMON_FROM_ROUTE5_DAYCARE,
id = SPECIAL_TAKE_POKEMON_FROM_ROUTE5_DAYCARE },
{ op = "end" },
})
eq(#s5.party, 2, "the POLIWHIRL came back to the party")
local back = s5.party[2]
eq(back and (back.species or back.speciesId), SPECIES_POLIWHIRL, "it is the POLIWHIRL")
eq(back and back.item, ITEM_FAB_MAIL, "GiveMailToMon2 put the FAB MAIL back in its hands")
check(Mail.monHasMail(back), "and it has its mail again")
eq(wordsOf(Mail.get(s5, back and back.mail)), depositedWords,
"the words came back unchanged")
eq(Daycare.route5Of(s5).mail, nil, "the day-care slot no longer holds a letter")
print("[test] 6. the two-slot day care keeps one letter per slot and shifts it")
local s6 = newSession()
Party.giveMon(s6, SPECIES_POLIWHIRL, 20, "POLI")
Party.giveMon(s6, SPECIES_ABRA, 20, "ABBY")
Party.giveMon(s6, SPECIES_ABRA, 20, "SPARE")
Mail.giveMailToMon(s6, s6.party[2], ITEM_FAB_MAIL)
runToEnd({
{ op = "setvar", [1] = VAR_0x8004, [2] = 0 },
{ op = "special", [1] = SPECIAL_STORE_SELECTED_POKEMON_IN_DAYCARE,
id = SPECIAL_STORE_SELECTED_POKEMON_IN_DAYCARE },
{ op = "setvar", [1] = VAR_0x8004, [2] = 0 },
{ op = "special", [1] = SPECIAL_STORE_SELECTED_POKEMON_IN_DAYCARE,
id = SPECIAL_STORE_SELECTED_POKEMON_IN_DAYCARE },
{ op = "end" },
})
local dc = Daycare.stateOf(s6)
eq(Daycare.count(dc), 2, "both mons are in the day care")
eq(dc.mail and dc.mail[1], nil, "slot 1, the POLIWHIRL, brought no letter")
check(dc.mail and dc.mail[2] ~= nil, "slot 2, the ABRA, did")
eq(dc.mail and dc.mail[2] and dc.mail[2].monName, "ABBY", "and it is that mon's letter")
runToEnd({
{ op = "setvar", [1] = VAR_0x8004, [2] = 0 },
{ op = "special", [1] = SPECIAL_TAKE_POKEMON_FROM_DAYCARE,
id = SPECIAL_TAKE_POKEMON_FROM_DAYCARE },
{ op = "end" },
})
dc = Daycare.stateOf(s6)
-- pokefirered/src/daycare.c:471
check(dc.mail and dc.mail[1] ~= nil, "ShiftDaycareSlots moved the letter into slot 1 with its mon")
eq(dc.mail and dc.mail[2], nil, "and cleared slot 2")
eq(dc.mail and dc.mail[1] and dc.mail[1].monName, "ABBY", "it is still the ABRA's letter")
runToEnd({
{ op = "setvar", [1] = VAR_0x8004, [2] = 0 },
{ op = "special", [1] = SPECIAL_TAKE_POKEMON_FROM_DAYCARE,
id = SPECIAL_TAKE_POKEMON_FROM_DAYCARE },
{ op = "end" },
})
local abby = nil
for _, mon in ipairs(s6.party) do
if mon.nickname == "ABBY" then abby = mon end
end
check(abby ~= nil, "the ABRA is back in the party")
eq(abby and abby.item, ITEM_FAB_MAIL, "holding its FAB MAIL again")
check(Mail.monHasMail(abby), "with its letter attached")
local leftover = Daycare.stateOf(s6).mail
eq(leftover and leftover[1], nil, "the day care kept no copy")
print("[test] 7. the pool survives a save round trip")
local s7 = newSession()
Party.giveMon(s7, SPECIES_POLIWHIRL, 20, "POLI")
Mail.giveMailToMon(s7, s7.party[1], ITEM_FAB_MAIL)
Mail.get(s7, 0).words[1] = ZYNX_WORDS[1]
local saved = Schema.toSaveTable(s7)
check(saved.mail ~= nil, "toSaveTable wrote the mail pool")
local loaded = Schema.fromSaveTable(saved)
eq(loaded.mail and loaded.mail[1] and loaded.mail[1].itemId, ITEM_FAB_MAIL,
"fromSaveTable restored the letter")
eq(loaded.mail and loaded.mail[1] and loaded.mail[1].words[1], ZYNX_WORDS[1],
"with its words")
eq(#(loaded.mail or {}), Mail.MAIL_COUNT, "as all sixteen SaveBlock1 slots")
eq(Mail.export({ party = {} }), nil, "an empty pool is not written into the save at all")
eq(Mail.restore(nil), nil, "and a save from before the pool existed restores to no pool")
eq(#Mail.pool({ party = {} }), Mail.MAIL_COUNT, "which the first letter creates on demand")
Runtime.session = prevSession
if failed > 0 then
print(string.format("FAILED %d check(s)", failed))
os.exit(1)
end
print("PASS game3_stitchscript_mail")
os.exit(0)