Files
gen1recomp/tests/online_trade_gen3.lua
2026-09-24 05:17:32 -04:00

247 lines
12 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
if not _G.love then _G.love = require("tests.love_stub") end
local Cache = require("tests.game3_cache")
if not Cache.mount("meta.json") then
print("[skip] online_trade_gen3: " .. tostring(Cache.reason))
os.exit(0)
end
local GameVersion = require("src.core.GameVersion")
GameVersion.set("firered")
local SaveData = require("src.core.SaveData")
local Trade = require("src.online.Trade")
local TeamPick = require("src.online.TeamPick")
local Pokemon = require("src.core.game3.pokemon")
local Party = require("src.core.game3.party")
local Mail = require("src.core.game3.mail")
local Schema = require("src.core.game3.save_schema_firered")
Pokemon.install(nil)
T.check(Pokemon._names ~= nil, "FireRed species tables load from the cache")
local files = {}
local fs = {
getInfo = function(name) return files[name] and { type = "file" } or nil end,
read = function(name) return files[name] end,
write = function(name, body) files[name] = body return true end,
remove = function(name) files[name] = nil return true end,
createDirectory = function() return true end,
}
SaveData.portableFs = function() return fs end
local DATA = { generation = 3, Pokemon = Pokemon }
local function makeSave(version, name, trainerId, mons, national)
local session = Schema.newGame({ version = version, name = name, rngSeed = trainerId })
session.trainerId = trainerId
session.party = {}
for _, row in ipairs(mons) do
assert(Party.giveMon(session, row.species, row.level, row.nickname))
local mon = session.party[#session.party]
for k, v in pairs(row.extra or {}) do mon[k] = v end
end
if national then session.dex.nationalUnlocked = true end
local save = Schema.toSaveTable(session)
return SaveData.decode(SaveData.encode(save))
end
local function handle(version, slotId, save)
return {
version = version, generation = 3, slotId = slotId, save = save,
path = Trade.slotPath(version, slotId), party = save.party, data = DATA,
}
end
local function copy(t)
if type(t) ~= "table" then return t end
local out = {}
for k, v in pairs(t) do out[k] = copy(v) end
return out
end
local function same(a, b)
if type(a) ~= "table" or type(b) ~= "table" then return a == b end
for k, v in pairs(a) do if not same(v, b[k]) then return false end end
for k in pairs(b) do if a[k] == nil then return false end end
return true
end
do
local fr = makeSave("firered", "RED", 11111, {
{ species = 64, level = 30, nickname = "ABRA CAD" },
{ species = 25, level = 12, extra = { item = 13, heldItem = 13 } },
}, true)
local lg = makeSave("leafgreen", "LEAF", 22222, {
{ species = 95, level = 25, extra = { item = 199, heldItem = 199 } },
{ species = 1, level = 8 },
}, false)
local A, B = handle("firered", "slot1", fr), handle("leafgreen", "slot1", lg)
local kadabra, onix = copy(fr.party[1]), copy(lg.party[1])
local plan, why = Trade.plan({ from = A, to = B, fromIndex = 1, toIndex = 1 })
T.check(plan ~= nil, "a FireRed <-> LeafGreen trade plans: " .. tostring(why))
T.eq(plan and plan.get.species, 208, "FireRed's Onix holding Metal Coat evolves into Steelix")
T.eq(plan and plan.get.item, 0, "the Metal Coat is used up")
T.eq(plan and plan.give.species, 65, "LeafGreen's Kadabra evolves into Alakazam")
T.eq(plan and plan.give.nickname, "ABRA CAD", "the nickname rides along")
T.eq(plan and plan.give.otName, "RED", "the OT name rides along")
T.eq(plan and plan.give.otId, kadabra.otId, "the OT id rides along")
T.eq(plan and plan.give.personality, kadabra.personality, "the personality rides along")
T.check(plan and same(plan.give.moves, kadabra.moves), "the moves ride along")
T.check(plan and same(plan.give.ivs, kadabra.ivs), "the IVs ride along")
T.eq(plan and plan.get.personality, onix.personality, "Onix keeps its personality")
T.eq(plan and plan.get.friendship, 70, "a traded mon's friendship is 70")
T.eq(plan and plan.give.friendship, 70, "on both sides")
T.eq(fr.party[1].species, 64, "planning leaves the FireRed save untouched")
local ok, err = Trade.commit(plan)
T.check(ok, "the trade commits: " .. tostring(err))
local frNow = SaveData.decode(files["saves/firered/slot1.lua"] or "")
local lgNow = SaveData.decode(files["saves/leafgreen/slot1.lua"] or "")
T.eq(frNow and frNow.party[1].species, 208, "the FireRed file holds Steelix")
T.eq(lgNow and lgNow.party[1].species, 65, "the LeafGreen file holds Alakazam")
T.eq(frNow and frNow.party[2].item, 13, "untraded party mons keep their items")
T.check(frNow and frNow.dex.owned[95] and frNow.dex.owned[208],
"FireRed's dex owns Onix and Steelix")
T.check(lgNow and lgNow.dex.owned[64] and lgNow.dex.owned[65],
"LeafGreen's dex owns Kadabra and Alakazam")
T.check(pcall(Schema.fromSaveTable, frNow), "the FireRed file loads back")
T.check(pcall(Schema.fromSaveTable, lgNow), "the LeafGreen file loads back")
T.eq(frNow and frNow.gameStats and frNow.gameStats[21], 1, "FireRed counts the trade")
T.eq(lgNow and lgNow.gameStats and lgNow.gameStats[21], 1, "LeafGreen counts the trade")
local function lastEvent(save)
local okS, session = pcall(Schema.fromSaveTable, save)
local scenes = okS and session.questLog and session.questLog.scenes or {}
local scene = scenes[#scenes]
return scene and scene.events[#scene.events], scene
end
local frEv, frScene = lastEvent(frNow)
T.eq(frEv and frEv.key, "TradedMon1ForPersonsMon2", "FireRed's quest log records the link trade")
T.eq(frEv and frEv.args.S1, "LEAF", "naming the partner")
T.eq(frEv and frEv.args.S2, Pokemon.name(95), "the mon received, before it evolved")
T.eq(frEv and frEv.args.S3, Pokemon.name(64), "and the mon sent")
T.eq(frScene and frScene.map, frNow and frNow.map, "on the save's map")
local lgEv = lastEvent(lgNow)
T.eq(lgEv and lgEv.args.S1, "RED", "LeafGreen's entry names RED")
T.eq(lgEv and lgEv.args.S2, Pokemon.name(64), "and the KADABRA it got")
end
do
local fr = makeSave("firered", "RED", 11111, {
{ species = 25, level = 12, extra = { item = 13, heldItem = 13 } },
{ species = 16, level = 5 },
}, false)
local lg = makeSave("leafgreen", "LEAF", 22222, {
{ species = 19, level = 7 },
{ species = 1, level = 8 },
}, false)
fr.mail = Mail.restore({})
local sessionLike = { mail = fr.mail, name = "RED", trainerId = 11111 }
fr.party[1].item, fr.party[1].heldItem = 0, 0
local id = Mail.giveMailToMon(sessionLike, fr.party[1], 121)
fr.mail[id + 1].words[1] = 1234
local A, B = handle("firered", "slot2", fr), handle("leafgreen", "slot2", lg)
local plan, why = Trade.plan({ from = A, to = B, fromIndex = 1, toIndex = 1 })
T.check(plan ~= nil, "a mail trade plans: " .. tostring(why))
local got = plan and plan.give
T.eq(got and got.item, 121, "the mail item rides along")
local sideB = plan and plan.sides[2]
local record = sideB and Mail.get(sideB.save3, got.mail)
T.eq(record and record.words[1], 1234, "the partner's mail lands in the receiver's pool")
local sideA = plan and plan.sides[1]
T.check(sideA and Mail.isEmpty(Mail.slot(sideA.save3, id)),
"the sender's copy of the mail is cleared")
T.eq(plan and plan.get.species, 19, "Rattata goes the other way")
end
do
local lone = makeSave("firered", "RED", 1, { { species = 25, level = 5 } }, false)
local lg = makeSave("leafgreen", "LEAF", 2, { { species = 19, level = 5 }, { species = 1, level = 5 } }, false)
local _, why = Trade.plan({ from = handle("firered", "s", lone),
to = handle("leafgreen", "s", lg), fromIndex = 1, toIndex = 1 })
T.eq(why, "that's your only POKéMON for battle", "the last mon can't be traded")
local mew = makeSave("firered", "RED", 1, {
{ species = 151, level = 5, extra = { fatefulEncounter = false } },
{ species = 25, level = 5 },
}, true)
_, why = Trade.plan({ from = handle("firered", "s", mew),
to = handle("leafgreen", "s", lg), fromIndex = 1, toIndex = 1 })
T.eq(why, "that POKéMON can't be traded now", "a Mew without the fateful bit is refused")
local johto = makeSave("firered", "RED", 1, {
{ species = 208, level = 30 }, { species = 25, level = 5 },
}, true)
_, why = Trade.plan({ from = handle("firered", "s", johto),
to = handle("leafgreen", "s", lg), fromIndex = 1, toIndex = 1 })
T.eq(why, "that POKéMON can't be traded now",
"a non-Kanto mon can't go to a partner without the National Dex")
local noNat = makeSave("firered", "RED", 1, {
{ species = 208, level = 30 }, { species = 25, level = 5 },
}, false)
_, why = Trade.plan({ from = handle("firered", "s", noNat),
to = handle("leafgreen", "s", lg), fromIndex = 1, toIndex = 1 })
T.eq(why, "that POKéMON can't be traded now",
"a non-Kanto mon can't leave a save without the National Dex")
local two = makeSave("firered", "RED", 1, { { species = 25, level = 5 }, { species = 16, level = 5 } }, false)
_, why = Trade.plan({ from = handle("firered", "s", two),
to = handle("leafgreen", "s", lg), fromIndex = { where = "box", box = 1, index = 1 }, toIndex = 1 })
T.eq(why, "that's not in the party", "FireRed trades come from the party only")
local red = { version = "red", generation = 1, slotId = "r", path = "saves/red/r.lua",
save = { party = { { species = "PIKACHU", level = 5, moves = {} } } } }
red.party = red.save.party
_, why = Trade.plan({ from = handle("firered", "s", two), to = red, fromIndex = 1, toIndex = 1 })
T.eq(why, "Those two games can't trade.", "FireRed and Red can't trade")
local remote, rwhy = Trade.remote(handle("firered", "s", two), { send = function() end })
T.eq(remote, nil, "an internet trade needs a seated room session")
T.eq(rwhy, "no seat", "and it says why")
local seated = Trade.remote(handle("firered", "s", two),
{ send = function() end, seat = function() return 0 end })
T.check(seated ~= nil and seated:stage() == "handshake",
"a FireRed save opens an internet trade")
local packed = TeamPick.pack({ party = two.party, generation = 3 }, { 2, 1 }, 3)
T.eq(packed and #packed, 2, "a FireRed team packs for a room")
T.eq(packed and packed[1].species, two.party[2].species, "in pick order")
T.eq(packed and packed[1].item, 0, "with the item always present")
local cpacked, cwhy = TeamPick.packConverted({}, { 1 }, 3)
T.eq(cpacked, nil, "Gen 3 never converts across generations")
T.eq(cwhy, TeamPick.NO_TIME_CAPSULE, "there is no Time Capsule")
end
do
local fr = makeSave("firered", "RED", 1, {
{ species = 25, level = 5 }, { species = 16, level = 5 },
}, true)
local lg = makeSave("leafgreen", "LEAF", 2, {
{ species = 152, level = 5, extra = { isEgg = true } }, { species = 1, level = 5 },
}, true)
local plan, why = Trade.plan({ from = handle("firered", "e", fr),
to = handle("leafgreen", "e", lg), fromIndex = 1, toIndex = 1 })
T.check(plan ~= nil, "an egg trades once both sides have the National Dex: " .. tostring(why))
local sideA = plan and plan.sides[1]
T.check(sideA and not (sideA.save3.dex.seen or {})[152],
"a received egg does not mark the dex")
T.eq(sideA and sideA.record.friendship, lg.party[1].friendship,
"egg friendship is left alone")
local scenes = sideA and sideA.save3.questLog and sideA.save3.questLog.scenes or {}
local ev = scenes[#scenes] and scenes[#scenes].events[1]
T.eq(ev and ev.args.S2, require("src.core.game3.rom_text").plain("gText_EggNickname"),
"the quest log names a received egg EGG")
local lgNoNat = makeSave("leafgreen", "LEAF", 2, {
{ species = 152, level = 5, extra = { isEgg = true } }, { species = 1, level = 5 },
}, false)
local _, ewhy = Trade.plan({ from = handle("firered", "e", fr),
to = handle("leafgreen", "e", lgNoNat), fromIndex = 1, toIndex = 1 })
T.check(ewhy ~= nil, "an egg can't leave a save without the National Dex")
end
T.finish()