Files
gen1recomp/tests/engine/trainer_id_sync.lua
T
2026-10-06 11:00:16 -04:00

172 lines
6.8 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = love or require("tests.love_stub")
local SaveData = require("src.core.SaveData")
local GameVersion = require("src.core.GameVersion")
local TrainerIdSync = require("src.core.TrainerIdSync")
local realFS = love.filesystem
local function memfs(files)
return {
files = files,
write = function(path, content) files[path] = content return true end,
read = function(path) return files[path] end,
remove = function(path) files[path] = nil return true end,
getInfo = function(path)
if files[path] then return { type = "file" } end
return nil
end,
}
end
local function fresh()
local files = {}
love.filesystem = memfs(files)
SaveData.resetSlotState()
GameVersion.set("red")
return files
end
do
local files = fresh()
local red = SaveData.createSlot("red")
SaveData.writeSlot("red", red, { version = "red", player = { id = 111, name = "RED" },
party = { { species = "PIKACHU", otId = 111, ot = "RED" } } })
local gold = SaveData.createSlot("gold")
SaveData.writeSlot("gold", gold, { version = "gold", generation = 2, player = { id = 222, name = "GOLD" },
party = { { species = "CYNDAQUIL", otId = 222, ot = "GOLD" }, { species = "ABRA", otId = 111, ot = "RED", traded = true } } })
local em = SaveData.createSlot("emerald")
SaveData.writeSlot("emerald", em, { version = "emerald", engine = "game3", generation = 3,
trainerId = 333, secretId = 5, name = "MAY", party = { { species = 25, otId = 333, otSecretId = 5, otName = "MAY", personality = 12345 } } })
local job = TrainerIdSync.newJob({ scope = "gold", slot = gold })
local steps, last = 0, -1
while not job:step() do
steps = steps + 1
local p = job:progress()
T.check(p >= last and p <= 1, "progress is monotonic")
last = p
if steps > 100 then break end
end
T.eq(job.error, nil, "job finishes without error")
T.eq(job.newId, 222, "source save's id is the target")
T.eq(job.written, 3, "both other saves plus the source (its traded-in Abra) are written")
local r = SaveData.decode(SaveData.readSlotSource("red", red))
T.eq(r.player.id, 222, "red save took gold's id")
T.eq(r.party[1].otId, 222, "red own mon followed")
T.check(type(r.meta) == "table" and r.meta.savedAt, "rewritten save is stamped for save sync")
local g = SaveData.decode(SaveData.readSlotSource("gold", gold))
T.eq(g.party[2].otId, 222, "mon traded in from the red save is synced into the source save")
local e = SaveData.decode(SaveData.readSlotSource("emerald", em))
T.eq(e.trainerId, 222, "emerald save took the id")
T.eq(e.party[1].otId, 222, "emerald own mon followed")
T.check(files ~= nil, "memfs used")
end
do
fresh()
local job = TrainerIdSync.newJob({ scope = "red", slot = "slot9" })
while not job:step() do end
T.check(job.error ~= nil, "a missing source save fails cleanly")
end
do
fresh()
local Json = require("src.link.Json")
local SyncState = require("src.sync.SyncState")
local SyncEngine = require("src.sync.SyncEngine")
local RomImporter = require("src.import.RomImporter")
local red = SaveData.createSlot("red")
SaveData.writeSlot("red", red, { version = "red", meta = { savedAt = 500 },
player = { id = 111, name = "RED" }, party = { { species = "PIKACHU", otId = 111, ot = "RED" } } })
local gold = SaveData.createSlot("gold")
SaveData.writeSlot("gold", gold, { version = "gold", generation = 2, meta = { savedAt = 500 },
player = { id = 222, name = "GOLD" }, party = { { species = "CYNDAQUIL", otId = 222, ot = "GOLD" } } })
local server, puts = {}, {}
local transport = { sent = {}, handles = {} }
function transport:begin(req)
self.sent[#self.sent + 1] = req
local path = req.url:match("^[^?]*"):gsub("^http://sync%.test", "")
local reply
if req.method == "GET" and path == "/sync/state" then
reply = { saves = server }
elseif req.method == "PUT" and path == "/sync/save" then
local body = Json.decode(req.body)
puts[#puts + 1] = body
local key = SyncState.key(body.version, body.meta.playthroughId)
local rev = ((server[key] or {}).rev or 0) + 1
server[key] = { rev = rev, meta = body.meta }
reply = { ok = true, rev = rev }
end
self.handles[#self.sent] = { status = "ok", code = reply and 200 or 404,
body = Json.encode(reply or { error = "no route" }) }
return #self.sent
end
function transport:poll(h) return self.handles[h] end
function transport:release() end
local state = SyncState.defaults()
state.account, state.deviceToken, state.enabled = "aa11bb22cc33dd44", "tok", true
local eng = SyncEngine.new({ baseUrl = "http://sync.test", transport = transport,
state = state, persist = false })
local function pump() for _ = 1, 60 do eng:update(0.05) end end
eng:syncNow()
pump()
T.eq(#puts, 2, "both saves reach the server before the ID sync")
puts = {}
eng:syncNow()
pump()
T.eq(#puts, 0, "an unchanged device uploads nothing")
local imp = {
_idSync = TrainerIdSync.newJob({ scope = "gold", slot = gold }),
_busy = { progress = 0 },
_clearBusy = function(self) self._busy = nil end,
_syncEngine = function() return eng end,
}
for _ = 1, 50 do
if not imp._idSync then break end
RomImporter._pumpIdSync(imp)
end
T.eq(imp._idSync, nil, "the launcher pump finishes the job")
T.check(imp._idSyncResult and imp._idSyncResult.body:find("Save sync is uploading", 1, true),
"the result says save sync picked it up")
pump()
T.eq(#puts, 1, "save sync uploads the rewritten save straight away")
local up = puts[1] or {}
T.eq(up.version, "red", "and it is the red save that changed")
local blob = SaveData.decode(up.blob or "")
T.eq(blob and blob.player.id, 222, "the uploaded blob carries the synced ID")
T.eq(blob and blob.party[1].otId, 222, "and the synced OT")
T.eq(up.baseRev, 1, "as a normal revision on top of the server copy, not a conflict")
T.eq(eng.phase, "idle", "and the engine settles")
end
do
local nick = ("50"):rep(11)
local ot = "91848350" .. ("00"):rep(7)
local struct = ("A5"):rep(12) .. "006F" .. ("00"):rep(19)
local save = { version = "red", player = { id = 111, name = "RED" }, party = {},
daycare = { cartRaw = "01" .. nick .. ot .. struct } }
local owners = {}
TrainerIdSync.addOwner(owners, { id = 111, name = "RED" }, 1)
local changed = TrainerIdSync.rewriteSave(save, 1, { id = 222, name = "BLUE", sid = 0 }, owners)
T.check(changed, "raw cart day-care block counts as a change")
local raw = save.daycare.cartRaw
T.eq(#raw, 112, "raw day-care block keeps its size")
T.eq(raw:sub(71, 74), "00DE", "raw day-care OT id rewritten")
T.eq(raw:sub(25, 34), "818B9484" .. "50", "raw day-care OT name rewritten")
T.eq(raw:sub(1, 24), "01" .. nick, "raw day-care nickname untouched")
end
love.filesystem = realFS
T.finish("trainer_id_sync")