mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
132 lines
4.6 KiB
Lua
132 lines
4.6 KiB
Lua
-- Loopback + lockstep helpers (21-testing-and-ci "link desync suite"),
|
|
-- generalized out of the hand-rolled harness in tests/run_link_tests.lua
|
|
-- so a modded battle can be driven the same way an unmodded one is.
|
|
--
|
|
-- Both sides run the whole engine locally off a shared seed; the only
|
|
-- thing crossing the wire is the turn's choice and a per-turn state hash.
|
|
-- If a mod changes battle math on one side only, the hashes diverge -- so
|
|
-- "the hashes agreed every turn" is the desync assertion, and the
|
|
-- handshake fingerprint is what is supposed to stop that battle starting.
|
|
|
|
local Net = require("src.link.Net")
|
|
local Protocol = require("src.link.Protocol")
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
local SaveData = require("src.core.SaveData")
|
|
|
|
local Link = {}
|
|
|
|
-- the 15-line stack pattern from tests/run_link_tests.lua:171-186
|
|
function Link.fakeGame(data, leadSpecies, opts)
|
|
opts = opts or {}
|
|
local save = SaveData.newGame()
|
|
local level = opts.level or 50
|
|
for _, species in ipairs(type(leadSpecies) == "table" and leadSpecies or { leadSpecies }) do
|
|
table.insert(save.party, Pokemon.new(data, species, level))
|
|
end
|
|
if opts.name then save.player.name = opts.name end
|
|
|
|
local stack = { list = {} }
|
|
function stack:push(state, ...)
|
|
table.insert(self.list, state)
|
|
if state.enter then state:enter(...) end
|
|
end
|
|
function stack:pop() table.remove(self.list) end
|
|
function stack:top() return self.list[#self.list] end
|
|
function stack:update(dt)
|
|
local top = self:top()
|
|
if top and top.update then top:update(dt) end
|
|
end
|
|
|
|
local Input = require("src.core.Input")
|
|
return { data = data, input = Input, stack = stack, save = save }
|
|
end
|
|
|
|
-- the engine bits a battle needs before any of this runs
|
|
function Link.prepare(data)
|
|
local Input = require("src.core.Input")
|
|
Input:init()
|
|
require("src.render.Font").load(data)
|
|
return Input
|
|
end
|
|
|
|
-- run a full lockstep battle over a loopback pair, mashing A on both
|
|
-- sides, and report whether any turn's hashes disagreed
|
|
function Link.lockstep(gameA, gameB, opts)
|
|
opts = opts or {}
|
|
local LinkBattle = require("src.link.LinkBattle")
|
|
local Input = require("src.core.Input")
|
|
|
|
local netA, netB = Net.loopbackPair()
|
|
local packedA = Protocol.packParty(gameA.save.party)
|
|
local packedB = Protocol.packParty(gameB.save.party)
|
|
local seed = opts.seed or 987654321
|
|
|
|
local battleA = LinkBattle.newHost(gameA, netA, {
|
|
myParty = packedA, theirParty = packedB,
|
|
theirName = gameB.save.player.name, seed = seed,
|
|
})
|
|
local battleB = LinkBattle.newGuest(gameB, netB, {
|
|
myParty = packedB, theirParty = packedA,
|
|
theirName = gameA.save.player.name, seed = seed,
|
|
})
|
|
|
|
local resA, resB
|
|
battleA.onFinish = function(r) resA = r end
|
|
battleB.onFinish = function(r) resB = r end
|
|
gameA.stack:push(battleA)
|
|
gameB.stack:push(battleB)
|
|
|
|
local guard, limit = 0, opts.maxFrames or 60000
|
|
while (resA == nil or resB == nil) and guard < limit do
|
|
guard = guard + 1
|
|
Input.pressed = { a = true }
|
|
gameA.stack:update(1 / 60)
|
|
gameB.stack:update(1 / 60)
|
|
end
|
|
|
|
-- a turn present on both sides with different hashes is the desync the
|
|
-- suite exists to catch
|
|
local mismatch
|
|
for turn, hash in pairs(battleA.localHashes) do
|
|
local other = battleB.localHashes[turn]
|
|
if other and other ~= hash then mismatch = mismatch or turn end
|
|
end
|
|
|
|
return {
|
|
battleA = battleA, battleB = battleB,
|
|
resultA = resA, resultB = resB,
|
|
frames = guard, completed = resA ~= nil and resB ~= nil,
|
|
desyncTurn = mismatch,
|
|
agreed = mismatch == nil,
|
|
}
|
|
end
|
|
|
|
-- convenience: build both sides and run, for the common symmetric case
|
|
function Link.pair(data, leadA, leadB, opts)
|
|
opts = opts or {}
|
|
Link.prepare(data)
|
|
local gameA = Link.fakeGame(data, leadA, { name = opts.nameA or "RED", level = opts.level })
|
|
local gameB = Link.fakeGame(data, leadB, { name = opts.nameB or "BLUE", level = opts.level })
|
|
return gameA, gameB
|
|
end
|
|
|
|
-- the two hellos the handshake compares. A one-sided mod moves the
|
|
-- fingerprint, which must land as "subset" -- and subset is not
|
|
-- battleAllowed, so the lockstep battle never starts instead of desyncing
|
|
-- into an unexplained draw.
|
|
function Link.handshake(gameA, gameB, modeA, modeB)
|
|
local Handshake = require("src.link.Handshake")
|
|
local helloA = Handshake.hello(gameA, modeA or "battle")
|
|
local helloB = Handshake.hello(gameB, modeB)
|
|
local verdict, reason = Handshake.checkCompat(helloA, helloB)
|
|
return {
|
|
helloA = helloA, helloB = helloB,
|
|
verdict = verdict, reason = reason,
|
|
match = helloA.fingerprint == helloB.fingerprint,
|
|
battleAllowed = Handshake.battleAllowed(verdict),
|
|
tradeAllowed = Handshake.tradeAllowed(verdict),
|
|
}
|
|
end
|
|
|
|
return Link
|