mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
262 lines
12 KiB
Lua
262 lines
12 KiB
Lua
-- The Gen 2 link surface, exercised against a REAL Gold boot.
|
|
--
|
|
-- POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/gold_link_fingerprint.lua love .
|
|
--
|
|
-- Gold cannot link (docs/gen2-link-design.md is the honest account of what that
|
|
-- would take), so this is not a link smoke test. It is the check that the
|
|
-- pieces which ARE built work against the extracted Gold dataset rather than
|
|
-- against a fixture:
|
|
--
|
|
-- 1. the dataset identifies itself as generation 2 with no help from
|
|
-- GameVersion, and Handshake.hello says so on the wire
|
|
-- 2. the Gen 2 fingerprint is stable, moves when a surface field moves, and
|
|
-- does NOT move when a non-surface field moves (the #511 lesson, checked
|
|
-- on Gold's own tables this time)
|
|
-- 3. a Gold peer and a Red peer refuse each other by generation instead of
|
|
-- pairing and desyncing
|
|
-- 4. every mon in a real Gold party survives packMon2 -> unpackMon2 with its
|
|
-- stats, experience, held item, happiness and derived shininess intact
|
|
--
|
|
-- A fixture test cannot say any of that, because the whole question is whether
|
|
-- the extracted tables and the engine agree.
|
|
local U = require("tests.drivers.util")
|
|
|
|
local Fingerprint = require("src.link.Fingerprint")
|
|
local Handshake = require("src.link.Handshake")
|
|
local Mon = require("src.battle.gen2.Mon")
|
|
local Protocol = require("src.link.Protocol")
|
|
|
|
return function(game)
|
|
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-link"
|
|
local failures = 0
|
|
|
|
local function check(ok, label)
|
|
if ok then
|
|
U.log("ok ", label)
|
|
else
|
|
failures = failures + 1
|
|
U.log("FAIL", label)
|
|
end
|
|
return ok
|
|
end
|
|
|
|
U.wait(45)
|
|
local data = game.data
|
|
assert(data and data.pokemon and next(data.pokemon), "gold data did not load")
|
|
|
|
-- ---- 1. generation, off the data alone
|
|
|
|
check(Fingerprint.generationOf(data) == 2,
|
|
"the Gold dataset reports itself as generation 2")
|
|
local hello = Handshake.hello(game, "trade")
|
|
check(hello.generation == 2, "the hello carries generation 2")
|
|
check(type(hello.fingerprint) == "string" and #hello.fingerprint == 16,
|
|
"the hello carries a 16-hex-digit Gen 2 fingerprint")
|
|
U.log(("fingerprint %s protocol %d engine %s"):format(
|
|
tostring(hello.fingerprint), hello.protocol, tostring(hello.engineVersion)))
|
|
|
|
-- ---- 2. stability and coverage
|
|
|
|
-- The baseline is recomputed with an EMPTY mod list, not taken from the
|
|
-- hello. Handshake.hello folds Handshake.mods(game) into the digest, and
|
|
-- every comparison below computes with {}, so on an install with one enabled
|
|
-- link-affecting mod the two differ by modKey alone -- which reads as "the
|
|
-- digest is not stable" and "catchRate moved the digest" for a reason having
|
|
-- nothing to do with the surface. The hello's own digest is asserted above;
|
|
-- from here on the baseline and the comparisons share a mod list.
|
|
Fingerprint.forget(data)
|
|
local base = Fingerprint.compute(data, {})
|
|
Fingerprint.forget(data)
|
|
check(Fingerprint.compute(data, {}) == base,
|
|
"the digest is stable across a forget/recompute")
|
|
|
|
-- The Gen 1 surface over the SAME tables must not collide with the Gen 2
|
|
-- one: checkCompat refuses a cross-generation pairing by the hello, and the
|
|
-- "[gen2]" tag is what makes the digest agree with that refusal.
|
|
Fingerprint.forget(data)
|
|
local asGen1 = Fingerprint.compute(data, {}, 1)
|
|
Fingerprint.forget(data)
|
|
check(asGen1 ~= base, "the Gen 1 and Gen 2 surfaces digest differently")
|
|
|
|
local function digestAfter(mutate, restore)
|
|
mutate()
|
|
Fingerprint.forget(data)
|
|
local value = Fingerprint.compute(data, {})
|
|
restore()
|
|
Fingerprint.forget(data)
|
|
return value
|
|
end
|
|
|
|
-- surface: a base stat, a move's power, a move's effect chance, a held
|
|
-- item's parameter, a growth curve coefficient. Each one changes a battle
|
|
-- turn or a trade rebuild, so each one must move the digest.
|
|
local species = data.pokemon.TOTODILE or data.pokemon.CYNDAQUIL
|
|
local before = species.baseStats.attack
|
|
check(digestAfter(function() species.baseStats.attack = before + 1 end,
|
|
function() species.baseStats.attack = before end) ~= base,
|
|
"a Gen 2 base stat moves the digest")
|
|
|
|
local move = data.moves.TACKLE
|
|
local movePower = move.power
|
|
check(digestAfter(function() move.power = movePower + 1 end,
|
|
function() move.power = movePower end) ~= base,
|
|
"a move's power moves the digest")
|
|
|
|
local chanceMove, chanceBefore
|
|
for _, id in ipairs({ "BODY_SLAM", "THUNDERBOLT", "ICE_BEAM" }) do
|
|
if data.moves[id] and data.moves[id].effectChance then
|
|
chanceMove, chanceBefore = data.moves[id], data.moves[id].effectChance
|
|
break
|
|
end
|
|
end
|
|
if chanceMove then
|
|
check(digestAfter(function() chanceMove.effectChance = chanceBefore + 1 end,
|
|
function() chanceMove.effectChance = chanceBefore end) ~= base,
|
|
"a move's effectChance moves the digest (Gen 2 only)")
|
|
else
|
|
U.log("skip no move with an effectChance in this dataset")
|
|
end
|
|
|
|
local held = data.gen2HeldItems and data.gen2HeldItems.LEFTOVERS
|
|
if held then
|
|
local heldBefore = held.heldParameter
|
|
check(digestAfter(function() held.heldParameter = (heldBefore or 0) + 1 end,
|
|
function() held.heldParameter = heldBefore end) ~= base,
|
|
"a held item's parameter moves the digest")
|
|
else
|
|
U.log("skip no LEFTOVERS held-item row in this dataset")
|
|
end
|
|
|
|
local curves = data.pokemon.growthRates
|
|
local curve = curves and (curves.GROWTH_MEDIUM_SLOW or select(2, next(curves)))
|
|
if curve then
|
|
local linearBefore = curve.linear
|
|
check(digestAfter(function() curve.linear = (linearBefore or 0) + 1 end,
|
|
function() curve.linear = linearBefore end) ~= base,
|
|
"a growth-curve coefficient moves the digest")
|
|
else
|
|
U.log("skip no growth-rate coefficient rows in this dataset")
|
|
end
|
|
|
|
-- NOT surface: catchRate (#511) and the constants index space. Either one
|
|
-- moving the digest would split two peers over something neither of their
|
|
-- simulations reads.
|
|
local catchBefore = species.catchRate
|
|
check(digestAfter(function() species.catchRate = (catchBefore or 0) + 1 end,
|
|
function() species.catchRate = catchBefore end) == base,
|
|
"catchRate does NOT move the digest")
|
|
|
|
if data.gen2Constants and data.gen2Constants.mapOrder then
|
|
local order = data.gen2Constants.mapOrder
|
|
local first = order[1]
|
|
check(digestAfter(function() order[1] = "NOT_A_MAP" end,
|
|
function() order[1] = first end) == base,
|
|
"the constants index space does NOT move the digest")
|
|
end
|
|
|
|
-- the per-record digests a subset trade negotiates on
|
|
local speciesRecords = Fingerprint.records(data, "pokemon")
|
|
local heldRecords = Fingerprint.records(data, "held_items")
|
|
check(speciesRecords.TOTODILE ~= nil and speciesRecords.growthRates == nil,
|
|
"per-species digests cover the species and not the growthRates sibling")
|
|
check(next(heldRecords) ~= nil, "per-held-item digests exist on Gold")
|
|
|
|
-- ---- 3. a Gold peer refuses a Red peer
|
|
|
|
local redHello = { type = "hello", protocol = hello.protocol,
|
|
name = "RED", generation = 1,
|
|
engineVersion = hello.engineVersion,
|
|
fingerprint = "0000000000000000", mods = {} }
|
|
local verdict, reason = Handshake.checkCompat(hello, redHello)
|
|
check(verdict == "refused" and reason == "generation_mismatch",
|
|
"a Gold hello refuses a Gen 1 peer by generation")
|
|
local oldHello = { type = "hello", name = "OLD" } -- pre-handshake build
|
|
check(Handshake.checkCompat(hello, oldHello) == "refused",
|
|
"a Gold hello refuses a pre-handshake build")
|
|
local lines = Handshake.describe(hello, redHello, "refused", "trade")
|
|
check(#lines > 0 and table.concat(lines, " "):find("generation"),
|
|
"the incompatibility screen names the generation")
|
|
for _, line in ipairs(lines) do U.log(" screen |" .. line) end
|
|
|
|
-- ---- 4. a real Gold party through the Gen 2 codec
|
|
|
|
local party = {}
|
|
for _, spec in ipairs({ { "TOTODILE", 12 }, { "PIDGEY", 7 },
|
|
{ "GEODUDE", 15 } }) do
|
|
local mon = Mon.new(data, spec[1], spec[2])
|
|
if mon then party[#party + 1] = mon end
|
|
end
|
|
check(#party == 3, "built a three-mon Gold party from the extracted tables")
|
|
-- a held item and a status, so the two fields the Gen 1 codec cannot carry
|
|
-- are actually under test
|
|
party[1].item = (data.items and data.items.LEFTOVERS) and "LEFTOVERS" or nil
|
|
party[1].status = "burn"
|
|
party[1].happiness = 137
|
|
party[1].pokerus = 0
|
|
party[1].ot, party[1].otId = "KRIS", 41234
|
|
party[2].hp = math.max(1, math.floor(party[2].maxHp / 2))
|
|
|
|
for i, mon in ipairs(party) do
|
|
local packed = Protocol.packMon2(mon)
|
|
local rebuilt, why = Protocol.unpackMon2(data, packed, { strict = true })
|
|
if not check(rebuilt ~= nil, ("slot %d rebuilds (%s)"):format(
|
|
i, tostring(why))) then break end
|
|
check(rebuilt.species == mon.species and rebuilt.level == mon.level,
|
|
("slot %d keeps species and level"):format(i))
|
|
check(rebuilt.experience == mon.experience,
|
|
("slot %d keeps experience (%s vs %s)"):format(
|
|
i, tostring(rebuilt.experience), tostring(mon.experience)))
|
|
check(rebuilt.hp == mon.hp and rebuilt.maxHp == mon.maxHp,
|
|
("slot %d keeps HP %s/%s"):format(i, tostring(rebuilt.hp),
|
|
tostring(rebuilt.maxHp)))
|
|
local same = true
|
|
for _, k in ipairs({ "hp", "attack", "defense", "speed",
|
|
"specialAttack", "specialDefense" }) do
|
|
if rebuilt.stats[k] ~= mon.stats[k] then same = false end
|
|
end
|
|
check(same, ("slot %d recomputes all six stats identically"):format(i))
|
|
check(rebuilt.shiny == mon.shiny and rebuilt.gender == mon.gender,
|
|
("slot %d re-derives shininess and gender from the DVs"):format(i))
|
|
check(#rebuilt.moves == #mon.moves, ("slot %d keeps its moveset"):format(i))
|
|
check(rebuilt.item == mon.item, ("slot %d keeps its held item (%s)"):format(
|
|
i, tostring(rebuilt.item)))
|
|
check(rebuilt.status == mon.status, ("slot %d keeps its status"):format(i))
|
|
check(rebuilt.happiness == mon.happiness,
|
|
("slot %d keeps its happiness"):format(i))
|
|
check(rebuilt.otId == mon.otId and rebuilt.ot == mon.ot,
|
|
("slot %d keeps its original trainer"):format(i))
|
|
end
|
|
|
|
-- the HP DV is derived, never sent: a packet that claims one is ignored
|
|
local tampered = Protocol.packMon2(party[1])
|
|
tampered.dvs.hp = 15
|
|
local rebuilt = Protocol.unpackMon2(data, tampered, { strict = true })
|
|
check(rebuilt and rebuilt.dvs.hp == Mon.hpDV(party[1].dvs),
|
|
"a claimed HP DV is ignored and re-derived from the other four")
|
|
|
|
-- an item the peer's game does not have is refused rather than carried
|
|
local noSuchItem = Protocol.packMon2(party[1])
|
|
noSuchItem.item = "MOON_FLUTE"
|
|
local _, itemWhy = Protocol.unpackMon2(data, noSuchItem, { strict = true })
|
|
check(itemWhy == "unknown item",
|
|
"an unknown held item is refused in strict mode")
|
|
|
|
-- and the subset filter says so BEFORE the mon is ever sent
|
|
local mine = { pokemon = Fingerprint.records(data, "pokemon"),
|
|
moves = Fingerprint.records(data, "moves"),
|
|
heldItems = Fingerprint.records(data, "held_items") }
|
|
local theirs = { pokemon = mine.pokemon, moves = mine.moves, heldItems = {} }
|
|
local eligible, reasons = Protocol.eligibleParty(party, mine, theirs)
|
|
check(eligible[1] == false and reasons[1] == "unknown item",
|
|
"the subset filter greys a mon whose held item the peer lacks")
|
|
check(eligible[2] == true, "a mon holding nothing stays tradeable")
|
|
|
|
-- ---- proof the game was actually up while all of that ran
|
|
|
|
U.shot(game, out .. "/01-gold-link-fingerprint.png")
|
|
U.log(("%d failures"):format(failures))
|
|
assert(failures == 0, ("gold link fingerprint driver: %d failures"):format(
|
|
failures))
|
|
U.log("PASS")
|
|
end
|