Files
gen1recomp/tests/game3_link_rs_cross_test.lua
T

263 lines
12 KiB
Lua

#!/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 FakeRelay = require("tests.g3link_fake_relay")
local store = { flags = {}, vars = {} }
local session = {
version = "ruby", store = store, map = "RS_TEST_POKEMON_CENTER_2F", x = 5, y = 3,
name = "BRENDAN", gender = 0, trainerId = 0x2222,
party = { { species = 280, level = 12 }, { species = 283, level = 9 } },
bag = { pockets = { items = {} } },
}
local game = { data = { maps = {} }, session = session, input = { wasPressed = function() return false end },
save = { player = { name = "BRENDAN" } } }
package.loaded["src.core.game3.runtime"] = {
getSession = function() return session end,
isActive = function() return true end,
_game = game,
}
package.loaded["src.core.game3.player"] = { cellX = 5, cellY = 3, facing = "up" }
package.loaded["src.core.game3.map"] = { load = function() end }
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,
translate = function(ir) return ir 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 ctx = { specialVars = {}, stringVars = {} }
local adapters = { log = function() end, playSe = function() end }
package.loaded["src.core.game3.scripting.space"] = {
store = store, mapId = session.map, vm = { ctx = ctx, adapters = adapters },
ensureBundle = function() return nil end,
}
package.loaded["src.online.ArenaData"] = {
liveProfile3 = function(_, rulesetId)
return { engine = 3, engineVersion = "1.0.0", fingerprint = "f00dcafe", kind = "vanilla",
version = session.version, rulesetId = rulesetId }
end,
}
local Client = FakeRelay.client({ state = "online", id = "0000beef" })
package.loaded["src.online.Client"] = Client
local Link = require("src.core.game3.link")
local LB = require("src.core.game3.link.battle")
local Flags = require("src.core.game3.scripting.flags")
local Lobby = require("src.ui.game3.rs.cable_lobby")
local Game3Link = require("src.link.Game3Link")
local RsNatives = require("src.core.game3.scripting.natives_link_rs").BY_NAME
local RseNatives = require("src.core.game3.scripting.natives_link_rse").BY_NAME
local ContestNatives = require("src.core.game3.scripting.natives_contest").BY_NAME
local CL = require("src.core.game3.link.contest_link")
local function setVar(id, v) Flags.setVar(store, ctx, id, v) end
local function getVar(id) return tonumber(Flags.getVar(store, ctx, id)) or 0 end
local function pick(id)
for _, row in ipairs(Lobby.rows()) do
if row.id == id and not row.disabled then return Lobby.opts.select(row) end
end
error("no enabled row " .. tostring(id))
end
local function pump(peers)
for _ = 1, 4 do
for _, p in ipairs(peers) do p:update(0) end
Link.update(0)
end
end
local function linkup(version, start, peerVersions, peerLinkType, max)
session.version = version
ctx.specialVars, ctx.nativePoll = {}, nil
Client.clear()
Client._group, Client._room, Client._seat = nil, nil, nil
Lobby.reset()
Link.reset()
Link._live = nil
local yielded = start()
if not yielded then return nil, "did not yield" end
pick("leader")
local members = { { id = "0000beef", name = session.name } }
for i = 1, #peerVersions do members[#members + 1] = { id = string.format("00e0000%d", i), name = "P" .. i } end
Client._group = { leader = "0000beef", min = 2, max = max, members = members, pending = {} }
ctx.nativePoll()
if Client.count("startGroup") == 0 then pick("start") end
local room = FakeRelay.room({ seats = #peerVersions + 1 })
Client.bindRoom(room, 0)
local peers = {}
for i, v in ipairs(peerVersions) do
local hello = Game3Link.hello(game, peerLinkType, { version = v, name = v:upper() .. i, trainerId = 0x3000 + i })
peers[i] = Game3Link.attach(FakeRelay.transport(room, i), {
game = game, linkType = peerLinkType, seat = i, seats = room.seats, hello = hello,
})
end
ctx.nativePoll()
pump(peers)
for _ = 1, LB.LINKUP_TICKS + 5 do
if ctx.nativePoll() then break end
end
return getVar(Link.VAR_RESULT), peers
end
local LT = Game3Link.LINKTYPE
print("[test] 1. trade: Ruby cable vs Emerald, both directions")
local code = linkup("ruby", function() return RsNatives.sub_80834E4(ctx, adapters) end,
{ "emerald" }, LT.TRADE_SETUP, 2)
eq(code, Link.LINKUP.SUCCESS, "Ruby's Trade Center links with an Emerald trader")
code = linkup("emerald", function() return RseNatives.TryTradeLinkup(ctx, adapters) end,
{ "sapphire" }, LT.TRADE_SETUP, 2)
eq(code, Link.LINKUP.SUCCESS, "Emerald's cable Trade Center links with a Sapphire trader")
print("[test] 2. battles: Ruby cable vs Emerald and FireRed wireless players")
code = linkup("ruby", function() setVar(0x8004, 1); return RsNatives.sub_808347C(ctx, adapters) end, { "emerald" }, LT.BATTLE, 2)
eq(code, Link.LINKUP.SUCCESS, "single battle with an Emerald Direct Corner player")
code = linkup("sapphire", function() setVar(0x8004, 2); return RsNatives.sub_808347C(ctx, adapters) end, { "firered" }, LT.BATTLE, 2)
eq(code, Link.LINKUP.SUCCESS, "double battle with a FireRed Direct Corner player")
code = linkup("ruby", function() setVar(0x8004, 5); return RsNatives.sub_808347C(ctx, adapters) end,
{ "emerald", "leafgreen", "sapphire" }, LT.BATTLE, 4)
eq(code, Link.LINKUP.SUCCESS, "multi battle with Emerald, LeafGreen and Sapphire")
code = linkup("emerald", function() setVar(0x8004, 1); return RseNatives.TryBattleLinkup(ctx, adapters) end, { "ruby" }, LT.SINGLE_BATTLE, 2)
eq(code, Link.LINKUP.SUCCESS, "Emerald's cable colosseum with a Ruby cable player")
code = linkup("emerald", function() setVar(0x8004, 1); return RseNatives.TryBattleLinkup(ctx, adapters) end, { "ruby" }, LT.DOUBLE_BATTLE, 2)
eq(code, Link.LINKUP.DIFF_SELECTIONS, "a single vs double cable pick is still LINKUP_DIFF_SELECTIONS")
print("[test] 3. record corner: Ruby with Emerald, three players")
code = linkup("ruby", function() return RsNatives.sub_808350C(ctx, adapters) end,
{ "emerald", "sapphire" }, LT.RECORD_MIX_BEFORE, 4)
eq(code, Link.LINKUP.SUCCESS, "Ruby's Record Corner links three players")
local players = Link.link and Link.link:players() or {}
eq(#players, 3, "all three seats are linked")
eq(players[2] and players[2].version, "emerald", "seat 1 is the Emerald player")
check(require("src.core.game3.link.rs_record_cross").anyRS(players), "Emerald sees an RS partner and uses the RS layout")
print("[test] 4. berry blender: Ruby and Emerald blend in step")
local blendPeers
code, blendPeers = linkup("ruby", function() return RsNatives.sub_8083614(ctx, adapters) end,
{ "emerald" }, LT.BERRY_BLENDER_SETUP, 4)
eq(code, Link.LINKUP.SUCCESS, "Ruby's blender links with an Emerald blender")
eq(Link.link and Link.link.linkType, LT.BERRY_BLENDER_SETUP, "the link runs as LINKTYPE_BERRY_BLENDER_SETUP")
local Blend = require("src.core.game3.rse.berry_blender_link")
local mine = Blend.new(Link.link, Link.link:players())
local theirs = Blend.new(blendPeers[1], blendPeers[1]:players())
eq(mine.rngState, theirs.rngState, "both machines seed the same blender RNG")
local a, b
for _ = 1, 8 do
a = a or mine:submitBerry(133)
b = b or theirs:submitBerry(140)
pump(blendPeers)
end
eq(a and a[1], 140, "Ruby sees the Emerald berry")
eq(b and b[0], 133, "Emerald sees the Ruby berry")
local fa, fb
for _ = 1, 8 do
fa = fa or mine:exchangeFrame(1, 2)
fb = fb or theirs:exchangeFrame(1, 3)
pump(blendPeers)
end
eq(fa and fa[1], 3, "the per-frame scores cross over")
eq(fb and fb[0], 2, "both ways")
print("[test] 5. contests: Ruby with three Emerald players")
code = linkup("ruby", function() setVar(0x8011, 0); return RsNatives.sub_808363C(ctx, adapters) end,
{ "emerald", "emerald", "emerald" }, 0x6602, 4)
eq(code, Link.LINKUP.SUCCESS, "Ruby's COOL contest links with three Emerald wireless contestants")
local flags = CL.flagsFor(Link.link, false)
eq(flags, CL.FLAG.IS_LINK + CL.FLAG.HAS_RS_PLAYER, "the contest is flagged HAS_RS_PLAYER")
code = linkup("emerald", function() setVar(0x8011, 0); return ContestNatives.TryContestEModeLinkup(ctx, adapters) end,
{ "ruby" }, 0x6601, 4)
eq(code, Link.LINKUP.SUCCESS, "Emerald's cable contest links with a Ruby contestant")
eq(CL.flagsFor(Link.link, true), CL.FLAG.IS_LINK + CL.FLAG.IS_WIRELESS + CL.FLAG.HAS_RS_PLAYER,
"and a wireless Emerald contest with a Ruby player skips the standby too")
session.version = "ruby"
ctx.specialVars, ctx.nativePoll = {}, nil
Client.clear()
Client._group, Client._room, Client._seat = nil, nil, nil
Lobby.reset()
Link.reset()
setVar(0x8011, 0)
RsNatives.sub_808363C(ctx, adapters)
pick("join")
Client._groups = { { leader = "00e00009", name = "MAY", avatar = { name = "MAY" }, joined = 1, max = 4 } }
pick("00e00009")
eq(Client.last("joinGroup") and Client.last("joinGroup")[1], "00e00009", "Ruby joins an Emerald wireless contest group")
local room2 = FakeRelay.room({ seats = 2 })
Client.bindRoom(room2, 1)
local lead = Game3Link.attach(FakeRelay.transport(room2, 0), { game = game, linkType = 0x6602, seat = 0, seats = 2,
hello = Game3Link.hello(game, 0x6602, { version = "emerald", name = "MAY", trainerId = 0x4444 }) })
ctx.nativePoll()
pump({ lead })
for _ = 1, LB.LINKUP_TICKS + 5 do
if ctx.nativePoll() then break end
end
eq(getVar(Link.VAR_RESULT), Link.LINKUP.WRONG_NUM_PLAYERS,
"a two player contest started by Emerald is LINKUP_WRONG_NUM_PLAYERS on Ruby")
Link.reset()
Lobby.reset()
print("[test] 6. record mixing data crosses Ruby and Emerald")
local function cacheRoot(version)
local env = os.getenv("POKEPORT_" .. version:upper() .. "_CACHE")
if env and env ~= "" then return env end
local identity, home = os.getenv("POKEPORT_" .. version:upper() .. "_IDENTITY") or os.getenv("POKEPORT_IDENTITY"), os.getenv("HOME")
if not (identity and identity ~= "" and home) then return nil end
for _, base in ipairs({ home .. "/Library/Application Support/LOVE/", home .. "/.local/share/love/" }) do
local root = base .. identity .. "/" .. version .. "/data/generated/gba"
local f = io.open(root .. "/meta.json", "rb")
if f then f:close(); return root end
end
return nil
end
local rubyRoot, emeraldRoot = cacheRoot("ruby"), cacheRoot("emerald")
if not (rubyRoot and emeraldRoot) then
print("[skip] 6. needs a Ruby and an Emerald cache (POKEPORT_RUBY_CACHE / POKEPORT_EMERALD_CACHE)")
else
local tmp = os.tmpname()
local function side(version, root, seat, out, inp)
local cmd = string.format("POKEPORT_GBA_CACHE=%q luajit tests/g3link_rs_mix_side.lua %s %d %q %s 2>&1",
root, version, seat, out, inp and string.format("%q", inp) or "")
local p = io.popen(cmd)
local text = p:read("*a")
p:close()
local out2 = {}
for k, v in text:gmatch("\n?(%w+) ([^\n]*)") do out2[k] = v end
return out2, text
end
local rubyOut, emOut = tmp .. ".ruby", tmp .. ".em"
local r1 = side("ruby", rubyRoot, 1, rubyOut)
local e = side("emerald", emeraldRoot, 0, emOut, rubyOut)
local r = side("ruby", rubyRoot, 1, rubyOut, emOut)
eq(e.layout, "rs", "Emerald sends the RS record layout to a Ruby partner")
check((tonumber(e.bytes) or 1e9) < 65536, "and the packet fits the relay's rse_record_mix cap (" .. tostring(e.bytes) .. ")")
check((tonumber(r1.bytes) or 1e9) < 65536, "Ruby's packet fits too (" .. tostring(r1.bytes) .. ")")
eq(e.unsupported, "nil", "Emerald takes Ruby's records")
eq(e.secretBases, "true", "Emerald mixes the secret bases")
eq(e.tvShows, "true", "and the TV shows")
eq(r.unsupported, "nil", "Ruby takes Emerald's records")
eq(r.oldMan, "true", "Ruby swaps the old man")
eq(r.daycareMail, "true", "and the daycare mail")
os.remove(tmp)
os.remove(rubyOut)
os.remove(emOut)
end
print(string.format("%s game3_link_rs_cross_test", failed == 0 and "PASS" or ("FAIL " .. failed)))
os.exit(failed == 0 and 0 or 1)