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

341 lines
9.7 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
local H = {}
local SHARED_PREFIX = { "src.import.", "tests." }
local SHARED = {
["src.link.Wire"] = true,
["src.link.Json"] = true,
["src.link.Protocol"] = true,
["src.core.Logger"] = true,
["src.core.Strings"] = true,
["src.core.game3.dataset"] = true,
["src.core.game3.pokemon"] = true,
["src.core.game3.rom_text"] = true,
["src.core.game3.items_data"] = true,
["src.core.game3.summary_data"] = true,
["src.core.game3.save_mon"] = true,
["src.core.game3.battle.moves"] = true,
}
local function swappable(name)
if type(name) ~= "string" or name:sub(1, 4) ~= "src." then return false end
if SHARED[name] then return false end
for _, prefix in ipairs(SHARED_PREFIX) do
if name:sub(1, #prefix) == prefix then return false end
end
return true
end
H.known = {}
H.current = nil
local function capture(w)
for name, mod in pairs(package.loaded) do
if swappable(name) then
H.known[name] = true
w.mods[name] = mod
end
end
end
local function clear()
for name in pairs(package.loaded) do
if swappable(name) then H.known[name] = true end
end
for name in pairs(H.known) do package.loaded[name] = nil end
end
function H.enter(w)
if H.current == w then return end
if H.current then capture(H.current) end
for name in pairs(H.known) do package.loaded[name] = w.mods[name] end
H.current = w
end
function H.run(w, fn, ...)
H.enter(w)
local out = { pcall(fn, ...) }
capture(w)
if not out[1] then error(out[2], 0) end
return unpack(out, 2)
end
H.bundle = nil
function H.mountCache()
local Cache = require("tests.game3_cache")
if H.bundle ~= nil then return H.bundle end
package.loaded["src.core.game3.scripting.space"] = nil
local bundle = Cache.bundle()
H.bundle = bundle or false
return H.bundle
end
function H.newWorld(name, session)
if H.current then capture(H.current) end
clear()
H.current = nil
local w = { name = name, mods = {}, session = session, reports = {} }
session.store = session.store or { flags = {}, vars = {} }
local game = { data = { maps = {} }, session = session }
w.game = game
package.loaded["src.core.game3.runtime"] = {
getSession = function() return w.session end,
isActive = function() return true end,
_game = game,
_mod = nil,
}
package.loaded["src.core.game3.player"] = { cellX = 0, cellY = 0, facing = "up" }
package.loaded["src.core.game3.map"] = { load = function() end, current = "FR_BATTLE_COLOSSEUM_2P" }
package.loaded["src.core.game3.objects"] = {
addObject = function() return true end,
removeObject = function() return true end,
refreshGraphics = function() return 0 end,
}
package.loaded["src.core.game3.scripting.space"] = {
store = session.store,
mapId = "FR_BATTLE_COLOSSEUM_2P",
ensureBundle = function() return H.bundle or nil end,
bundle = H.bundle or nil,
}
package.loaded["src.online.Client"] = {
report = function(result) w.reports[#w.reports + 1] = result end,
state = function() return "online" end,
}
w.Link = require("src.core.game3.link")
w.LB = require("src.core.game3.link.battle")
w.Battle = require("src.core.game3.battle")
w.Ui = require("src.core.game3.battle.ui")
w.Commands = require("src.core.game3.battle.commands")
w.State = require("src.core.game3.battle.state")
w.Game3Link = require("src.link.Game3Link")
w.RelayTransport = require("src.core.game3.link.relay_transport")
w.Guard = require("src.core.game3.battle.link_guard")
capture(w)
H.current = w
return w
end
local Json = require("src.link.Json")
local function wireCopy(msg)
return Json.decode(Json.encode(msg))
end
local Relay = {}
Relay.__index = Relay
function H.relay(opts)
opts = opts or {}
local self = setmetatable({
room = "r" .. string.format("%016x", opts.roomSeed or 1),
seats = opts.seats or 2,
seed = opts.seed or 1,
match = opts.match or "m1",
players = {},
ends = {},
log = {},
lag = opts.lag or {},
tamper = opts.tamper,
}, Relay)
for seat = 0, self.seats - 1 do
self.players[#self.players + 1] = { id = string.format("%08x", seat + 1), name = "P" .. seat, seat = seat, online = true }
end
return self
end
function Relay:client()
local relay = self
return {
state = function() return "online" end,
error = function() return nil end,
room = function()
return { room = relay.room, seats = relay.seats, players = relay.players, seed = relay.seed,
match = relay.match }
end,
seat = function() return nil end,
role = function() return nil end,
}
end
local Session = {}
Session.__index = Session
function Relay:session(seat)
local s = setmetatable({
relay = self, mySeat = seat, inbox = {}, pending = {},
paired = true, closed = false, left = false, target = self.room,
}, Session)
self.ends[#self.ends + 1] = s
for _, row in ipairs(self.log) do
if row.seat ~= seat then
local msg = wireCopy(row.msg)
msg.seat = row.seat
s.inbox[#s.inbox + 1] = msg
end
end
return s
end
function Relay:deliver(fromSeat, msg)
self.log[#self.log + 1] = { seat = fromSeat, msg = wireCopy(msg) }
for _, s in ipairs(self.ends) do
if s.mySeat ~= fromSeat and not s.left then
local copy = wireCopy(msg)
copy.seat = fromSeat
if self.tamper then copy = self.tamper(fromSeat, s.mySeat, copy) or copy end
local delay = self.lag[s.mySeat == nil and "spectator" or s.mySeat] or 0
s.pending[#s.pending + 1] = { at = delay, msg = copy }
end
end
end
function Relay:tick()
for _, s in ipairs(self.ends) do
local keep = {}
for _, row in ipairs(s.pending) do
row.at = row.at - 1
if row.at < 0 and row.gone ~= nil then
s.gone = s.gone or {}
s.gone[row.gone] = true
elseif row.at < 0 then
s.inbox[#s.inbox + 1] = row.msg
else
keep[#keep + 1] = row
end
end
s.pending = keep
end
end
function Relay:drop(seat)
for i = #self.players, 1, -1 do
if self.players[i].seat == seat then table.remove(self.players, i) end
end
end
function Relay:leave(seat)
for _, s in ipairs(self.ends) do
if s.mySeat ~= seat and not s.left then
local delay = self.lag[s.mySeat == nil and "spectator" or s.mySeat] or 0
s.pending[#s.pending + 1] = { at = delay, gone = seat }
end
end
end
function Session:update() end
function Session:seat() return self.mySeat end
function Session:seats() return self.relay.seats end
function Session:players()
if not self.gone then return self.relay.players end
local out = {}
for _, p in ipairs(self.relay.players) do
if not self.gone[p.seat] then out[#out + 1] = p end
end
return out
end
function Session:role()
if self.mySeat == nil then return "spectator" end
return ({ [0] = "host", [1] = "guest", [2] = "seat2", [3] = "seat3" })[self.mySeat]
end
function Session:match() return self.relay.match end
function Session:seed() return self.seedOverride or self.relay.seed end
function Session:peerOnline() return true end
function Session:send(msg)
if self.closed or self.mySeat == nil or type(msg) ~= "table" then return end
self.relay:deliver(self.mySeat, msg)
end
function Session:poll()
local out = self.inbox
self.inbox = {}
return out
end
function Session:take(messageType, predicate)
for i, msg in ipairs(self.inbox) do
if msg.type == messageType and (predicate == nil or predicate(msg) == true) then
table.remove(self.inbox, i)
return msg
end
end
return nil
end
function Session:close()
if self.mySeat ~= nil and not self.left then self.relay:leave(self.mySeat) end
self.left = true
self.closed = true
end
function Session:hasPending() return #self.inbox > 0 end
H.Relay = Relay
function H.legal(mon)
local Pokemon = require("src.core.game3.pokemon")
local SummaryData = require("src.core.game3.summary_data")
mon.exp = mon.exp or SummaryData.expForLevel(Pokemon.growthRate(mon.species), mon.level)
mon.otName = mon.otName or "RED"
mon.otId = mon.otId or 0x1234
mon.personality = mon.personality or 0
if mon.hp == nil then
Pokemon.applyStats(mon)
mon.hp = mon.maxHp
end
return mon
end
function H.pack(list)
local Protocol = require("src.link.Protocol")
local out = {}
for i, mon in ipairs(list) do out[i] = Protocol.packMon3(H.legal(mon)) end
return out
end
function H.attachSeat(w, relay, seat, spec)
return H.run(w, function()
local rs = relay:session(seat)
rs.seedOverride = spec.seedOverride
local t = w.RelayTransport.new(rs, { client = relay:client() })
local lk = w.Game3Link.attach(t, { seat = seat, seats = relay.seats, game = w.game,
linkType = w.LB.arenaLinkType(w.LB.MODE_OF[spec.mode or "single"]) })
w.Link.attach(lk)
w.transport = t
spec.session = rs
spec.seat = seat
spec.seats = relay.seats
spec.seed = relay.seed
spec.headless = true
if spec.autoFight == nil then spec.autoFight = false end
w.LB.startArena(spec, function(result) w.result = result end)
return lk
end)
end
function H.attachSpectator(w, relay, spec)
return H.run(w, function()
local rs = relay:session(nil)
local t = w.RelayTransport.new(rs, { client = relay:client() })
w.transport = t
spec.session = rs
spec.transport = t
spec.seed = relay.seed
spec.headless = true
w.LB.startSpectator(spec, function(result) w.result = result end)
end)
end
function H.step(w, policy)
return H.run(w, function()
w.Link.update(1 / 60)
if w.LB._spec then w.LB.update(1 / 60) end
local Battle = w.Battle
if Battle.isActive() then
if policy and Battle._phase == "command" and not Battle._st.spectate and w.Ui._pendingCommand == nil then
policy(w, Battle._st)
end
Battle.update(0, nil)
end
end)
end
return H