mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-28 23:00:21 -04:00
388 lines
17 KiB
Lua
388 lines
17 KiB
Lua
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
love = love or require("tests.love_stub")
|
|
|
|
local HOME = os.getenv("HOME") or ""
|
|
local LOVE_ROOTS = { HOME .. "/Library/Application Support/LOVE", HOME .. "/.local/share/love" }
|
|
|
|
local function metaOk(root, version, cacheVersion)
|
|
local f = io.open(root .. "/data/generated/gba/meta.json", "rb")
|
|
if not f then return false end
|
|
local src = (f:read("*a") or ""):gsub("%s", "")
|
|
f:close()
|
|
return src:find(('"cache_version":%d'):format(cacheVersion), 1, true) ~= nil
|
|
and (version ~= "emerald" or src:find('"version":"emerald"', 1, true) ~= nil)
|
|
end
|
|
|
|
local function findCache(version, cacheVersion, preferred)
|
|
for _, r in ipairs(LOVE_ROOTS) do
|
|
local names = {}
|
|
if preferred and preferred ~= "" then names[#names + 1] = preferred end
|
|
local pipe = io.popen('ls -1t "' .. r .. '" 2>/dev/null')
|
|
if pipe then
|
|
for name in pipe:lines() do names[#names + 1] = name end
|
|
pipe:close()
|
|
end
|
|
for _, name in ipairs(names) do
|
|
local root = r .. "/" .. name .. "/" .. version
|
|
if metaOk(root, version, cacheVersion) then return root, name end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local VersionsGame = require("src.import.gba.versions_game")
|
|
local identity = os.getenv("POKEPORT_IDENTITY")
|
|
local emHere = false
|
|
for _, r in ipairs(LOVE_ROOTS) do
|
|
if identity and identity ~= "" and metaOk(r .. "/" .. identity .. "/emerald", "emerald",
|
|
VersionsGame.game("emerald").CACHE_VERSION) then emHere = true end
|
|
end
|
|
if not emHere and os.getenv("POKEPORT_XVER_CHILD") ~= "1" then
|
|
local _, id = findCache("emerald", VersionsGame.game("emerald").CACHE_VERSION)
|
|
if not id then
|
|
print("emerald_link_xver_test: skipped (no current Emerald cache)")
|
|
os.exit(0)
|
|
end
|
|
local status = os.execute(("POKEPORT_XVER_CHILD=1 POKEPORT_IDENTITY='%s' %s tests/emerald_link_xver_test.lua")
|
|
:format(id, os.getenv("LUA") or "luajit"))
|
|
os.exit((status == true or status == 0) and 0 or 1)
|
|
end
|
|
|
|
local T = require("tests.harness")
|
|
T.verbose = true
|
|
local check, eq = T.check, T.eq
|
|
|
|
local GameVersion = require("src.core.GameVersion")
|
|
local Profile = require("src.core.game3.profile")
|
|
Profile.reset()
|
|
GameVersion.set("emerald")
|
|
|
|
local Dataset = require("src.core.game3.dataset")
|
|
local emCache = Dataset.cache()
|
|
local meta = emCache and emCache.read and emCache:read("data/generated/gba/meta.json")
|
|
if not (type(meta) == "string" and meta:find('"version":"emerald"', 1, true)) then
|
|
print("emerald_link_xver_test: skipped (no Emerald cache; set POKEPORT_IDENTITY)")
|
|
os.exit(0)
|
|
end
|
|
require("src.import.gba.versions").select("emerald")
|
|
require("src.core.game3.song_ids").select("emerald")
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
Pokemon.install(nil)
|
|
|
|
local ROOTS = {
|
|
emerald = findCache("emerald", VersionsGame.game("emerald").CACHE_VERSION, identity),
|
|
firered = findCache("firered", VersionsGame.game("firered").CACHE_VERSION, os.getenv("POKEPORT_FR_IDENTITY")),
|
|
leafgreen = findCache("leafgreen", VersionsGame.game("leafgreen").CACHE_VERSION, os.getenv("POKEPORT_LG_IDENTITY")),
|
|
}
|
|
for _, v in ipairs({ "emerald", "firered", "leafgreen" }) do
|
|
if not ROOTS[v] then
|
|
print("emerald_link_xver_test: skipped (no current " .. v .. " cache)")
|
|
os.exit(0)
|
|
end
|
|
print("[info] " .. v .. " cache at " .. ROOTS[v])
|
|
end
|
|
|
|
local function reader(root)
|
|
return function(rel)
|
|
local f = io.open(root .. "/" .. rel, "rb")
|
|
if not f then return nil end
|
|
local s = f:read("*a")
|
|
f:close()
|
|
return s
|
|
end
|
|
end
|
|
|
|
local Fingerprint = require("src.link.Fingerprint")
|
|
local Game3Link = require("src.link.Game3Link")
|
|
local Json = require("src.link.Json")
|
|
local Wire = require("src.link.Wire")
|
|
local HostRules = require("src.core.game3.link.host_rules")
|
|
|
|
local INPUTS = {}
|
|
for v, root in pairs(ROOTS) do INPUTS[v] = Fingerprint.gen3Inputs(reader(root)) end
|
|
|
|
local KINESIS, NATURE_POWER, SNATCH, PSYCHIC, BODY_SLAM, THUNDERBOLT, CROSS_CHOP = 134, 267, 289, 94, 34, 85, 238
|
|
local DIFF = { KINESIS, NATURE_POWER, SNATCH }
|
|
local FAMILY = { firered = "frlg", leafgreen = "frlg", emerald = "rse" }
|
|
|
|
print("[test] 1. the three battle_moves.h rows differ, the rest of the surface does not")
|
|
local frM, emM, lgM = INPUTS.firered.moves, INPUTS.emerald.moves, INPUTS.leafgreen.moves
|
|
check(frM[NATURE_POWER].accuracy ~= emM[NATURE_POWER].accuracy, "NATURE_POWER accuracy differs FR/EM")
|
|
check(frM[KINESIS].flags ~= emM[KINESIS].flags, "KINESIS flags differ FR/EM")
|
|
check(frM[SNATCH].flags ~= emM[SNATCH].flags, "SNATCH flags differ FR/EM")
|
|
local diffRows = 0
|
|
for id, row in pairs(frM) do
|
|
for _, f in ipairs(Fingerprint.GEN3_MOVE_FIELDS) do
|
|
if emM[id] == nil or emM[id][f] ~= row[f] then diffRows = diffRows + 1 break end
|
|
end
|
|
end
|
|
eq(diffRows, 3, "exactly three move rows differ")
|
|
eq(Fingerprint.movesGen3Of(lgM), Fingerprint.movesGen3Of(frM), "LeafGreen's rows are FireRed's")
|
|
local function dataOf(v) return { generation = 3, gen3Inputs = INPUTS[v] } end
|
|
eq(Fingerprint.coreGen3(dataOf("firered")), Fingerprint.coreGen3(dataOf("emerald")), "the non-move surface is identical")
|
|
check(Fingerprint.compute(dataOf("firered"), {}, 3) ~= Fingerprint.compute(dataOf("emerald"), {}, 3),
|
|
"the full fingerprints still differ")
|
|
|
|
print("[test] 2. the handshake pairs cross-family battles on host rules")
|
|
local function hello(v, linkType)
|
|
local s = { version = v, name = "P", trainerId = 1, gender = 0, dex = {}, store = { flags = {}, vars = {} } }
|
|
return Game3Link.hello({ version = v, data = dataOf(v), session = s, save = { player = { name = "P" } } }, linkType,
|
|
{ session = s })
|
|
end
|
|
local LT = Game3Link.LINKTYPE
|
|
local frH, emH, lgH = hello("firered", LT.SINGLE_BATTLE), hello("emerald", LT.SINGLE_BATTLE), hello("leafgreen", LT.SINGLE_BATTLE)
|
|
local function sane(h) return Wire.sanitize(Json.decode(Json.encode(h))) end
|
|
eq(sane(frH).game3.core, frH.game3.core, "Wire carries the core digest")
|
|
eq(sane(frH).game3.moves, frH.game3.moves, "Wire carries the moves digest")
|
|
local verdict, why, cross, hosted = Game3Link.decideOne(emH, sane(frH), LT.SINGLE_BATTLE)
|
|
eq(verdict, "full", "EM sees FR: a battle pairs (" .. tostring(why) .. ")")
|
|
eq(cross, true, "cross-family")
|
|
eq(hosted, true, "on host rules")
|
|
verdict, why, cross, hosted = Game3Link.decideOne(frH, sane(emH), LT.DOUBLE_BATTLE)
|
|
eq(verdict, "full", "FR sees EM: a double pairs (" .. tostring(why) .. ")")
|
|
eq(hosted, true, "on host rules")
|
|
verdict, why, cross, hosted = Game3Link.decideOne(frH, sane(lgH), LT.SINGLE_BATTLE)
|
|
eq(verdict, "full", "FR<->LG pairs as today")
|
|
eq(hosted, false, "FR<->LG needs no host rules")
|
|
eq(cross, false, "FR<->LG is one family")
|
|
local modded = sane(emH)
|
|
modded.linkModified = true
|
|
verdict, why = Game3Link.decideOne(frH, modded, LT.SINGLE_BATTLE)
|
|
eq(verdict, "refused", "a link-modifying mod still fails closed")
|
|
local otherCore = sane(emH)
|
|
otherCore.game3.core = ("0"):rep(16)
|
|
verdict, why = Game3Link.decideOne(frH, otherCore, LT.SINGLE_BATTLE)
|
|
eq(verdict, "refused", "a different species/ability/item surface is refused")
|
|
eq(why, "fingerprint_mismatch", "as a fingerprint mismatch")
|
|
|
|
print("[test] 3. the host rules block round-trips and is checked")
|
|
do
|
|
local block = { version = "firered", rules = Fingerprint.rulesGen3("firered"), moves = Fingerprint.movesGen3Of(frM), rows = {} }
|
|
local ids = {}
|
|
for id in pairs(frM) do ids[#ids + 1] = id end
|
|
table.sort(ids)
|
|
for _, id in ipairs(ids) do block.rows[#block.rows + 1] = HostRules.encodeRow(id, frM[id]) end
|
|
local wired = Wire.sanitize(Json.decode(Json.encode({ type = "game3_battle_setup", hostRules = block, party = {} })))
|
|
local decoded, err = HostRules.decode(wired.hostRules, frH.game3.moves)
|
|
check(decoded ~= nil, "a FireRed block decodes after Wire.sanitize: " .. tostring(err))
|
|
eq(decoded and decoded.rows[NATURE_POWER].accuracy, frM[NATURE_POWER].accuracy, "FireRed NATURE_POWER accuracy arrives")
|
|
local _, bad = HostRules.decode(wired.hostRules, emH.game3.moves)
|
|
eq(bad, "host_rules_rows", "rows that don't match the host hello are refused")
|
|
wired.hostRules.rules = Fingerprint.rulesGen3("emerald")
|
|
_, bad = HostRules.decode(wired.hostRules)
|
|
eq(bad, "host_rules_mismatch", "a rules digest that doesn't match the named game is refused")
|
|
end
|
|
|
|
print("[test] 4. cross-version link battles over the relay")
|
|
local H = require("tests.link3_harness")
|
|
H.SHARED["src.core.game3.battle.moves"] = nil
|
|
local ExtractScripts = require("src.import.gba.extract_scripts")
|
|
local Cache = require("tests.game3_cache")
|
|
H.bundle = ExtractScripts.loadBundle(Cache.cache(), ROOTS.emerald .. "/data/generated/gba", { allowIncomplete = true })
|
|
|
|
local function cacheFor(v)
|
|
local read = reader(ROOTS[v])
|
|
return { read = function(_, rel) return read(rel) end }
|
|
end
|
|
|
|
local function mon(species, moves)
|
|
return H.legal({ species = species, level = 50, moves = moves, personality = 0,
|
|
ivs = { hp = 20, atk = 20, def = 20, spe = 20, spa = 20, spd = 20 },
|
|
evs = { hp = 0, atk = 0, def = 0, spe = 0, spa = 0, spd = 0 }, item = 0 })
|
|
end
|
|
|
|
local function partyA()
|
|
return H.pack({ mon(65, { KINESIS, NATURE_POWER, SNATCH, PSYCHIC }), mon(143, { NATURE_POWER, KINESIS, SNATCH, BODY_SLAM }) })
|
|
end
|
|
local function partyB()
|
|
return H.pack({ mon(94, { KINESIS, NATURE_POWER, SNATCH, THUNDERBOLT }), mon(68, { SNATCH, NATURE_POWER, KINESIS, CROSS_CHOP }) })
|
|
end
|
|
|
|
local function moveNum(m)
|
|
if type(m) == "table" then m = m.id or m.move or m.num end
|
|
return tonumber(m)
|
|
end
|
|
|
|
local function observe(w, st)
|
|
local BP = require("src.core.game3.battle.profile")
|
|
local Moves = require("src.core.game3.battle.moves")
|
|
w.seen = w.seen or {
|
|
hostRules = st.hostRules,
|
|
family = BP.of(st).family,
|
|
rulesFrom = BP.of(st).rulesFrom,
|
|
focusPunch = BP.rule(st, "obedienceFocusPunchExempt"),
|
|
npAcc = Moves.get(NATURE_POWER).accuracy,
|
|
kinesisFlags = Moves.get(KINESIS).flags,
|
|
snatchFlags = Moves.get(SNATCH).flags,
|
|
}
|
|
end
|
|
|
|
local function usedMove(w, mon0, slot)
|
|
w.used = w.used or {}
|
|
local id = mon0.moves and moveNum(mon0.moves[slot])
|
|
if id then w.used[id] = true end
|
|
end
|
|
|
|
local function singles(w, st)
|
|
observe(w, st)
|
|
local m = st.player and st.player.mon or {}
|
|
local usable = {}
|
|
for i = 1, 4 do
|
|
if m.moves and m.moves[i] and (tonumber(m.pp and m.pp[i]) or 0) > 0 then usable[#usable + 1] = i end
|
|
end
|
|
local slot = usable[((st.turn or 0) % math.max(1, #usable)) + 1] or 1
|
|
usedMove(w, m, slot)
|
|
w.Ui._pendingCommand = w.Commands.playerAction(st, 1, slot)
|
|
end
|
|
|
|
local function doubles(w, st)
|
|
observe(w, st)
|
|
local sel = w.Battle._dblSel
|
|
if not (sel and sel.active) then return end
|
|
local id = sel.active
|
|
local b = w.State.battler(st, id)
|
|
local m = b and b.mon or {}
|
|
local usable = {}
|
|
for i = 1, 4 do
|
|
if m.moves and m.moves[i] and (tonumber(m.pp and m.pp[i]) or 0) > 0 then usable[#usable + 1] = i end
|
|
end
|
|
local slot = usable[(((st.turn or 0) + id) % math.max(1, #usable)) + 1] or 1
|
|
usedMove(w, m, slot)
|
|
local target = w.State.isPresent(st, 1) and 1 or 3
|
|
w.Ui._pendingCommand = w.Commands.playerAction(st, 1, slot, id, target)
|
|
end
|
|
|
|
local seq = 0
|
|
local function world(v, name, tid, gender)
|
|
seq = seq + 1
|
|
local w = H.newWorld(name .. seq, { name = name, trainerId = tid, gender = gender, party = {}, bag = {}, version = v })
|
|
w.version = v
|
|
w.game.version = v
|
|
w.game.data = { maps = {}, generation = 3, gen3Inputs = INPUTS[v] }
|
|
H.run(w, function()
|
|
require("src.core.GameVersion").set("emerald")
|
|
require("src.core.game3.song_ids").select("emerald")
|
|
local Moves = require("src.core.game3.battle.moves")
|
|
local load = Moves.loadRomPack
|
|
Moves.loadRomPack = function() return load(cacheFor(v)) end
|
|
Moves.loadRomPack()
|
|
end)
|
|
return w
|
|
end
|
|
|
|
local MIRROR = { win = "lose", lose = "win", draw = "draw" }
|
|
|
|
local function match(hostV, guestV, mode, seed)
|
|
local label = ("%s host vs %s guest (%s)"):format(hostV, guestV, mode)
|
|
print("[info] " .. label)
|
|
local relay = H.relay({ seed = seed, seats = 2, roomSeed = seed })
|
|
local w0 = world(hostV, "HOST", 0x1234, 0)
|
|
local w1 = world(guestV, "GUEST", 0x5678, 1)
|
|
local l0 = H.attachSeat(w0, relay, 0, { mode = mode, myParty = partyA(), profile = { rule = {} } })
|
|
local l1 = H.attachSeat(w1, relay, 1, { mode = mode, myParty = partyB(), profile = { rule = {} } })
|
|
local policy = mode == "double" and doubles or singles
|
|
local frames = 0
|
|
while frames < 30000 do
|
|
frames = frames + 1
|
|
H.step(w0, policy)
|
|
H.step(w1, policy)
|
|
relay:tick()
|
|
if w0.result and w1.result then break end
|
|
end
|
|
local same = hostV == guestV or FAMILY[hostV] == FAMILY[guestV]
|
|
and Fingerprint.movesGen3Of(INPUTS[hostV].moves) == Fingerprint.movesGen3Of(INPUTS[guestV].moves)
|
|
eq(H.run(w0, function() return l0.hostRules end), not same, label .. ": host link hostRules")
|
|
eq(H.run(w1, function() return l1.hostRules end), not same, label .. ": guest link hostRules")
|
|
local setupBytes, carried = 0, false
|
|
for _, row in ipairs(relay.log) do
|
|
if row.msg.type == "game3_battle_setup" then
|
|
setupBytes = math.max(setupBytes, #Json.encode(row.msg))
|
|
if row.seat == 0 and row.msg.hostRules then carried = true end
|
|
end
|
|
end
|
|
eq(carried, not same, label .. ": the host setup carries its rules block only when needed")
|
|
check(setupBytes < 32768, label .. ": setup fits the relay's 32768-byte cap (" .. setupBytes .. ")")
|
|
check(w0.result ~= nil and w1.result ~= nil, label .. ": both seats finished (" .. frames .. " frames)")
|
|
eq(MIRROR[w0.result], w1.result, label .. ": the results mirror (" .. tostring(w0.result) .. "/" .. tostring(w1.result) .. ")")
|
|
eq(H.run(w0, function() return w0.LB.endReason end), nil, label .. ": seat 0 saw no desync")
|
|
eq(H.run(w1, function() return w1.LB.endReason end), nil, label .. ": seat 1 saw no desync")
|
|
local h0 = H.run(w0, function() return w0.LB._myHashes end) or {}
|
|
local h1 = H.run(w1, function() return w1.LB._myHashes end) or {}
|
|
local n, ok = 0, true
|
|
for turn, v in pairs(h0) do
|
|
if h1[turn] ~= nil then
|
|
n = n + 1
|
|
if h1[turn] ~= v then ok = false end
|
|
end
|
|
end
|
|
check(ok and n >= 3, label .. ": every turn's digest matched across seats (" .. n .. " turns)")
|
|
local hostRow = INPUTS[hostV].moves
|
|
for _, w in ipairs({ w0, w1 }) do
|
|
local s = w.seen or {}
|
|
local who = (w == w0) and "host" or "guest"
|
|
eq(s.family, FAMILY[hostV], label .. ": " .. who .. " ran on the host's battle family")
|
|
eq(s.npAcc, hostRow[NATURE_POWER].accuracy, label .. ": " .. who .. " NATURE_POWER accuracy is the host's")
|
|
eq(s.kinesisFlags, hostRow[KINESIS].flags, label .. ": " .. who .. " KINESIS flags are the host's")
|
|
eq(s.snatchFlags, hostRow[SNATCH].flags, label .. ": " .. who .. " SNATCH flags are the host's")
|
|
eq(s.focusPunch, FAMILY[hostV] == "frlg", label .. ": " .. who .. " Focus Punch obedience rule is the host's")
|
|
end
|
|
eq(w1.seen and w1.seen.hostRules, (not same) and hostV or nil, label .. ": guest battle state names the host game")
|
|
for _, id in ipairs(DIFF) do
|
|
check((w0.used or {})[id] and (w1.used or {})[id], label .. ": both seats used move " .. id)
|
|
end
|
|
eq(H.run(w1, function() return require("src.core.game3.battle.moves").get(NATURE_POWER).accuracy end),
|
|
INPUTS[guestV].moves[NATURE_POWER].accuracy, label .. ": the guest's own rows are back after the battle")
|
|
end
|
|
|
|
match("firered", "emerald", "single", 0x4242)
|
|
match("emerald", "firered", "single", 0x1717)
|
|
match("firered", "emerald", "double", 0x5A5A)
|
|
match("leafgreen", "emerald", "single", 0x2323)
|
|
match("emerald", "leafgreen", "single", 0x3131)
|
|
match("firered", "leafgreen", "single", 0x6464)
|
|
|
|
print("[test] 5. the host rows decide the outcome (MAGIC_COAT vs KINESIS)")
|
|
local MAGIC_COAT, MIRROR_MOVE = 277, 119
|
|
local function coatMatch(sabotage)
|
|
local relay = H.relay({ seed = 0x7777, seats = 2, roomSeed = 0x7777 })
|
|
local w0 = world("firered", "HOST", 0x1234, 0)
|
|
local w1 = world("emerald", "GUEST", 0x5678, 1)
|
|
if sabotage then
|
|
H.run(w1, function()
|
|
w1.LB.applyHostRules = function()
|
|
require("src.core.game3.link.host_rules").clear()
|
|
return nil
|
|
end
|
|
end)
|
|
end
|
|
local a = H.pack({ mon(65, { KINESIS, KINESIS, KINESIS, KINESIS }) })
|
|
local b = H.pack({ mon(143, { MAGIC_COAT, MAGIC_COAT, MIRROR_MOVE, BODY_SLAM }) })
|
|
H.attachSeat(w0, relay, 0, { mode = "single", myParty = a, profile = { rule = {} } })
|
|
H.attachSeat(w1, relay, 1, { mode = "single", myParty = b, profile = { rule = {} } })
|
|
local frames = 0
|
|
while frames < 30000 do
|
|
frames = frames + 1
|
|
H.step(w0, singles)
|
|
H.step(w1, singles)
|
|
relay:tick()
|
|
if w0.result and w1.result then break end
|
|
end
|
|
local h0 = H.run(w0, function() return w0.LB._myHashes end) or {}
|
|
local h1 = H.run(w1, function() return w1.LB._myHashes end) or {}
|
|
local n, same = 0, true
|
|
for turn, v in pairs(h0) do
|
|
if h1[turn] ~= nil then
|
|
n = n + 1
|
|
if h1[turn] ~= v then same = false end
|
|
end
|
|
end
|
|
local reason = H.run(w1, function() return w1.LB.endReason end) or H.run(w0, function() return w0.LB.endReason end)
|
|
return same and reason == nil, n, reason
|
|
end
|
|
local okHost, nHost = coatMatch(false)
|
|
check(okHost and nHost >= 1, "on the host's rows both seats agree when KINESIS meets MAGIC_COAT (" .. nHost .. " turns)")
|
|
local okOwn, _, why = coatMatch(true)
|
|
check(not okOwn, "a guest that ignored the host rows would desync (" .. tostring(why) .. ")")
|
|
|
|
T.finish("emerald_link_xver")
|