Files
gen1recomp/tests/game3_link_rs_entry_test.lua
T

305 lines
14 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_OLDALE_TOWN_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 input = { pressed = {} }
function input:wasPressed(k) return self.pressed[k] == true end
function input:isDown(k) return self.pressed[k] == true end
local game = { data = { maps = {} }, session = session, input = input, 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,
}
local LIVE = { engine = 3, engineVersion = "1.0.0", fingerprint = "f00dcafe", kind = "vanilla" }
package.loaded["src.online.ArenaData"] = {
liveProfile3 = function(_, rulesetId)
local p = { version = session.version }
for k, v in pairs(LIVE) do p[k] = v end
p.rulesetId = rulesetId
return p
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 Union = require("src.core.game3.link.union_room")
local Flags = require("src.core.game3.scripting.flags")
local Lobby = require("src.ui.game3.rs.cable_lobby")
local Game3Link = require("src.link.Game3Link")
local NativesLinkRs = require("src.core.game3.scripting.natives_link_rs")
local function getVar(id) return tonumber(Flags.getVar(store, ctx, id)) or 0 end
local function setVar(id, v) Flags.setVar(store, ctx, id, v) end
local function fresh()
ctx.specialVars = {}
ctx.nativePoll, ctx.mode, ctx.status = nil, nil, nil
Client.clear()
Client._group, Client._groups, Client._directEntries = nil, {}, {}
Client._room, Client._seat = nil, nil
Lobby.reset()
Link.reset()
Link._live = nil
end
local function rowIds()
local out = {}
for i, row in ipairs(Lobby.rows()) do out[i] = row.id end
return out
end
local function pick(id)
for _, row in ipairs(Lobby.rows()) do
if row.id == id then return Lobby.opts.select(row) end
end
error("no row " .. tostring(id))
end
local function poll(n)
local r
for _ = 1, n or 1 do
r = ctx.nativePoll()
if r then break end
end
return r
end
local function me(id) return { id = id or "0000beef", name = "BRENDAN", avatar = { name = "BRENDAN" } } end
local function peerOn(room, seat, version, linkType)
local hello = Game3Link.hello(game, linkType, { version = version, name = version:upper(), trainerId = 0x1000 + seat })
return Game3Link.attach(FakeRelay.transport(room, seat), {
game = game, linkType = linkType, seat = seat, seats = room.seats, hello = hello,
})
end
local function pump(peers)
for _ = 1, 4 do
for _, p in ipairs(peers) do p:update(0) end
Link.update(0)
end
end
print("[test] 1. a Ruby cable leader auto-accepts and starts a two-seat group")
fresh()
setVar(0x8004, 1)
local yielded = NativesLinkRs.BY_NAME.sub_808347C(ctx, adapters)
check(yielded, "sub_808347C parks the script")
check(Lobby.isOpen(), "the cable lobby is up")
eq(table.concat(rowIds(), ","), "leader,join", "CREATE GROUP / JOIN GROUP")
pick("leader")
local og = Client.last("openGroup")
eq(og and og[1], "battle_single", "the leader opens a battle_single group")
eq(og and og[2] and og[2].rulesetId, "g3_single", "with the battle ruleset profile")
Client._group = { leader = "0000beef", min = 2, max = 2, members = { me() },
pending = { { id = "00e00001", name = "MAY", avatar = { name = "MAY" } } } }
poll()
local acc = Client.last("acceptGroup")
eq(acc and acc[1], "00e00001", "the first group_request is accepted without a prompt")
eq(acc and acc[2], true, "with ok = true")
Client._group = { leader = "0000beef", min = 2, max = 2, members = { me(), { id = "00e00001", name = "MAY" } }, pending = {} }
poll()
eq(Client.count("startGroup"), 1, "a full two-seat group starts by itself")
print("[test] 2. an Emerald wireless peer seated into the group links up with no linkup message")
local room = FakeRelay.room({ seats = 2 })
Client.bindRoom(room, 0)
local peer = peerOn(room, 1, "emerald", Game3Link.LINKTYPE.BATTLE)
check(poll() == false, "the match opens the relay link")
check(Link.link ~= nil, "Link.openRelay ran")
pump({ peer })
check(Link.link and Link.link:isReady(), "the hellos agree")
local done = poll(LB.LINKUP_TICKS + 5)
check(done == true, "the linkup finishes")
eq(getVar(Link.VAR_RESULT), Link.LINKUP.SUCCESS, "Ruby vs an Emerald wireless player is LINKUP_SUCCESS")
eq(LB.state, "seat", "and the colosseum seat wait begins")
eq(Link.link and Link.link.linkType, 0x2233, "the link runs as LINKTYPE_SINGLE_BATTLE")
check(not Lobby.isOpen(), "the lobby closed")
print("[test] 3. a Ruby joiner sees Direct Corner hosts next to cable groups")
fresh()
setVar(0x8004, 2)
NativesLinkRs.BY_NAME.sub_808347C(ctx, adapters)
pick("join")
eq(Client.last("groupList") and Client.last("groupList")[1], "battle_double", "it watches battle_double groups")
eq(Client.last("directList") and Client.last("directList")[1], "battle_double", "and the Direct Corner list")
Client._groups = { { leader = "00c00001", name = "RED", avatar = { name = "RED" }, joined = 1, max = 2 } }
Client._directEntries.battle_double = {
{ kind = "room", room = "r00000000000000aa", host = "00d00001", name = "LEAF", avatar = { name = "LEAF" } },
{ kind = "room", room = "r00000000000000bb", host = "00d00002", name = "PIN", locked = true },
{ kind = "player", id = "00d00003", name = "AUTO" },
}
local rows = Lobby.rows()
eq(#rows, 3, "one group and two hosted rooms (queued players are not joinable)")
eq(rows[2].kind, "room", "the Direct Corner host is listed")
eq(rows[3].disabled, true, "a PIN room is not joinable from the cable")
pick("r00000000000000aa")
local jr = Client.last("joinRoom")
eq(jr and jr[1], "r00000000000000aa", "room_join to the Direct Corner host")
eq(jr and jr[2], "player", "as a player")
Lobby.opts.cancel()
eq(Client.count("leaveRoom"), 1, "B while joining leaves the room")
check(Lobby.isOpen(), "and goes back to the list")
print("[test] 4. FireRed's cable counter falls back to the relay instead of timing out")
fresh()
session.version = "firered"
local NativesLink = require("src.core.game3.scripting.natives_link")
setVar(0x8004, Link.USING.TRADE_CENTER)
yielded = NativesLink.BY_NAME.TryTradeLinkup(ctx, adapters)
check(yielded, "TryTradeLinkup parks the script")
check(Lobby.isOpen(), "with the cable lobby, not a dead wait")
for _ = 1, LB.LINKUP_TICKS + 5 do ctx.nativePoll() end
eq(getVar(Link.VAR_RESULT), Link.LINKUP.ONGOING, "and it does not time out to CONNECTION_ERROR")
pick("leader")
eq(Client.last("openGroup") and Client.last("openGroup")[1], "trade", "the trade group")
Lobby.opts.cancel()
eq(getVar(Link.VAR_RESULT), Link.LINKUP.FAILED, "B is LINKUP_FAILED")
eq(Client.count("leaveGroup"), 1, "and leaves the group")
print("[test] 5. Emerald's cable counters pick the matching relay activity")
session.version = "emerald"
local cases = {
{ "TryBattleLinkup", Link.USING.SINGLE_BATTLE, "battle_single" },
{ "TryBattleLinkup", Link.USING.MULTI_BATTLE, "battle_multi" },
{ "TryBattleLinkup", Link.USING.BATTLE_TOWER, "battle_tower" },
{ "TryRecordMixLinkup", 0, "record_corner" },
}
local NativesLinkRse = require("src.core.game3.scripting.natives_link_rse")
for _, c in ipairs(cases) do
fresh()
setVar(0x8004, c[2])
NativesLinkRse.BY_NAME[c[1]](ctx, adapters)
pick("leader")
eq(Client.last("openGroup") and Client.last("openGroup")[1], c[3], c[1] .. " " .. c[2] .. " opens " .. c[3])
end
fresh()
NativesLinkRse.BY_NAME.TryBerryBlenderLinkup(ctx, adapters)
pick("leader")
eq(Client.last("openGroup") and Client.last("openGroup")[1], "berry_blender", "TryBerryBlenderLinkup opens berry_blender")
fresh()
setVar(0x8011, 2)
local NativesContest = require("src.core.game3.scripting.natives_contest")
NativesContest.BY_NAME.TryContestEModeLinkup(ctx, adapters)
pick("leader")
eq(Client.last("openGroup") and Client.last("openGroup")[1], "contest_cute", "a CUTE link contest opens contest_cute")
eq(LB.towerSpec().linkType, 0x2266, "a level 50 tower challenge links as LINKTYPE_BATTLE_TOWER_50")
print("[test] 6. a 2..4 group still asks the leader")
fresh()
setVar(0x8004, 0)
NativesLinkRse.BY_NAME.TryRecordMixLinkup(ctx, adapters)
pick("leader")
Client._group = { leader = "0000beef", min = 2, max = 4, members = { me() },
pending = { { id = "00e00002", name = "WALLY", avatar = { name = "WALLY" } } } }
poll()
eq(table.concat(rowIds(), ","), "yes,no", "ADD WALLY? YES / NO")
eq(Client.count("acceptGroup"), 0, "nothing accepted yet")
pick("yes")
eq(Client.last("acceptGroup") and Client.last("acceptGroup")[2], true, "YES accepts")
print("[test] 7. the activity names follow each game's sLinkGroupActivityNameTexts")
session.version = "emerald"
eq(Union.ACTIVITY_NAMES[15], "sLinkGroupActivityNameTexts[15]", "Emerald RECORD CORNER")
eq(Union.ACTIVITY_NAMES[23], "sLinkGroupActivityNameTexts[23]", "Emerald COOL CONTEST")
eq(Union.ACTIVITY_NAMES[28], "sLinkGroupActivityNameTexts[28]", "Emerald BATTLE TOWER LV. 50")
eq(Union.ACTIVITY_NAMES[13], nil, "Emerald SPIN TRADE is an empty string")
eq(Union.ACTIVITY_NAMES[3], "sLinkGroupActivityNameTexts[3]", "MULTI BATTLE")
session.version = "firered"
eq(Union.ACTIVITY_NAMES[13], "sLinkGroupActivityNameTexts[13]", "FireRed SPIN TRADE")
eq(Union.ACTIVITY_NAMES[15], nil, "FireRed has no RECORD CORNER name")
eq(Union.ACTIVITY_NAMES[10], "sLinkGroupActivityNameTexts[10]", "BERRY CRUSH")
print("[test] 8. the Direct Corner CHOOSE list shows open cable groups")
Client._directEntries.trade = {
{ kind = "group", leader = "00f00001", name = "BRENDAN", avatar = { name = "BRENDAN", version = "ruby" }, joined = 1, max = 2 },
{ kind = "group", leader = "00f00002", name = "FULL", avatar = { name = "FULL" }, joined = 2, max = 2 },
}
local drows = Union.directRows("trade")
eq(#drows, 1, "the open group is listed, the full one is not")
eq(drows[1] and drows[1].kind, "group", "as a group row")
eq(drows[1] and drows[1].leader, "00f00001", "with its leader id")
eq(drows[1] and drows[1].version, "ruby", "and the Ruby version for the trade check")
print("[test] 9. record mixing packs byte runs so a full Emerald packet fits the relay cap")
local RecordMix = require("src.core.game3.link.record_mix")
local Json = require("src.link.Json")
local Wire = require("src.link.Wire")
local raw = {}
for i = 1, 160 do raw[i] = (i * 37) % 256 end
local packet = { secretBases = { { trainerName = "MAY", _recordMixNativeBytes = raw } }, tvShows = { [0] = { kind = 1 } } }
local msg = Wire.sanitize(Json.decode(Json.encode({ type = RecordMix.MSG.PACKET, spot = 1, packet = RecordMix.toWire(packet) })))
local back = RecordMix.fromWire(msg.packet)
local same = true
for i = 1, 160 do if back.secretBases[1]._recordMixNativeBytes[i] ~= raw[i] then same = false end end
check(same, "160 native bytes survive the wire")
eq(back.tvShows[0].kind, 1, "zero-indexed rows still round trip")
check(#Json.encode(RecordMix.toWire(packet)) < 700, "and the byte run travels as one hex string")
print("[test] 10. a contest with a Ruby player is flagged and skips the wireless standby")
local CL = require("src.core.game3.link.contest_link")
local fakeLink = {
isOpen = function() return true end,
players = function() return { { seat = 0, version = "emerald" }, { seat = 1, version = "ruby" } } end,
send = function(self, m) self.sent = (self.sent or 0) + 1 end,
take = function() return nil end,
getSeat = function() return 0 end,
}
local flags = CL.flagsFor(fakeLink, true)
eq(flags, CL.FLAG.IS_LINK + CL.FLAG.IS_WIRELESS + CL.FLAG.HAS_RS_PLAYER, "IS_LINK | IS_WIRELESS | HAS_RS_PLAYER")
local s = CL.newSession(fakeLink, { flags = flags })
eq(s:standby(), true, "LinkContest_TryLinkStandby skips the standby with an RS player")
eq(fakeLink.sent, nil, "without sending a standby block")
CL.active = s
NativesContest.linkFlags = flags
eq(select(2, NativesContest.BY_NAME.IsContestWithRSPlayer()), 1, "IsContestWithRSPlayer answers TRUE")
eq(select(1, NativesContest.linkContestWaitForConnection(ctx, adapters)), false,
"LinkContestWaitForConnection does not park the script for an RS contest")
eq(CL.flagsFor({ players = function() return { { seat = 0, version = "emerald" } } end }, false), CL.FLAG.IS_LINK,
"an all-Emerald cable contest is just IS_LINK")
CL.reset()
NativesContest.linkFlags = 0
Link.reset()
Lobby.reset()
print(string.format("%s game3_link_rs_entry_test", failed == 0 and "PASS" or ("FAIL " .. failed)))
os.exit(failed == 0 and 0 or 1)