mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-08 01:34:06 -04:00
234 lines
9.7 KiB
Lua
234 lines
9.7 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 ids = { [100] = true, [200] = true, [300] = true }
|
|
local save = {
|
|
version = "red",
|
|
player = { id = 200, name = "RED" },
|
|
party = {
|
|
{ species = "PIKACHU", otId = 200 },
|
|
{ species = "ABRA", otId = 100, traded = true },
|
|
{ species = "MEW", otId = 4242, traded = true },
|
|
{ species = "ODDISH" },
|
|
},
|
|
boxes = { { { species = "ZUBAT", otId = 300 } } },
|
|
daycare = { mon = { species = "EKANS", otId = 200 } },
|
|
}
|
|
T.check(TrainerIdSync.rewriteSave(save, 1, 100, ids), "gen1 save reports a change")
|
|
T.eq(save.player.id, 100, "gen1 player id takes the new id")
|
|
T.eq(save.party[1].otId, 100, "own catch follows the id")
|
|
T.eq(save.party[2].otId, 100, "mon traded from another save follows the id")
|
|
T.eq(save.party[2].traded, nil, "and is no longer flagged traded")
|
|
T.eq(save.party[3].otId, 4242, "a real trade keeps its OT")
|
|
T.eq(save.party[3].traded, true, "and stays traded")
|
|
T.eq(save.party[4].otId, nil, "an unstamped mon is left for load-time stamping")
|
|
T.eq(save.boxes[1][1].otId, 100, "boxed mons follow the id")
|
|
T.eq(save.daycare.mon.otId, 100, "daycare mon follows the id")
|
|
T.check(not TrainerIdSync.rewriteSave(save, 1, 100, ids), "a second pass is a no-op")
|
|
end
|
|
|
|
do
|
|
local raw = ("00"):rep(12) .. "00C8" .. ("00"):rep(30)
|
|
local save = { player = { id = 200 }, party = { { cartRaw = raw } } }
|
|
TrainerIdSync.rewriteSave(save, 1, 100, { [100] = true, [200] = true })
|
|
T.eq(save.party[1].cartRaw:sub(25, 28), "0064", "raw gen1 carrier gets its OT bytes rewritten")
|
|
T.eq(#save.party[1].cartRaw, #raw, "without changing its length")
|
|
end
|
|
|
|
do
|
|
local save = {
|
|
version = "gold", player = { id = 200 },
|
|
party = { { otId = 200, traded = true } },
|
|
dayCare = { man = { mon = { otId = 200 } }, egg = { otId = 200 } },
|
|
hallOfFame = { teams = { { mons = { { otId = 200 } } } } },
|
|
mail = { party = { { authorId = 200 } }, box = { { authorId = 9 } } },
|
|
}
|
|
TrainerIdSync.rewriteSave(save, 2, 100, { [100] = true, [200] = true })
|
|
T.eq(save.player.id, 100, "gen2 player id")
|
|
T.eq(save.dayCare.man.mon.otId, 100, "gen2 daycare parent")
|
|
T.eq(save.dayCare.egg.otId, 100, "gen2 daycare egg")
|
|
T.eq(save.hallOfFame.teams[1].mons[1].otId, 100, "gen2 hall of fame")
|
|
T.eq(save.mail.party[1].authorId, 100, "gen2 own mail")
|
|
T.eq(save.mail.box[1].authorId, 9, "gen2 foreign mail untouched")
|
|
end
|
|
|
|
do
|
|
local bit = require("bit")
|
|
local function shinyKey(tid, sid) return bit.bxor(tid, sid) end
|
|
local save = {
|
|
engine = "game3", version = "emerald", trainerId = 200, secretId = 0x1234,
|
|
party = { { otId = 200, otSecretId = 0x1234 }, { otId = 300, otSecretId = 0x0F0F } },
|
|
storage = { boxes = { [3] = { mons = { [7] = { otId = 200, otSecretId = 0x1234 } } } } },
|
|
modData = { emerald_daycare = { daycare = { { otId = 200, otSecretId = 0x1234 } } } },
|
|
hallOfFameTeams = { { { trainerId = 200, otSecretId = 0x1234 } } },
|
|
secretBases = { { trainerId = { 200, 0, 0x34, 0x12 } } },
|
|
}
|
|
TrainerIdSync.rewriteSave(save, 3, 100, { [100] = true, [200] = true, [300] = true })
|
|
T.eq(save.trainerId, 100, "gen3 trainer id")
|
|
T.eq(shinyKey(save.trainerId, save.secretId), shinyKey(200, 0x1234),
|
|
"gen3 secret id is reseated so own shinies stay shiny")
|
|
T.eq(save.party[1].otId, 100, "gen3 party mon")
|
|
T.eq(save.party[1].otSecretId, save.secretId, "own mon keeps matching the save's secret id")
|
|
T.eq(shinyKey(save.party[2].otId, save.party[2].otSecretId), shinyKey(300, 0x0F0F),
|
|
"mon from another save keeps its shininess")
|
|
T.eq(save.storage.boxes[3].mons[7].otId, 100, "gen3 sparse box mon")
|
|
T.eq(save.modData.emerald_daycare.daycare[1].otId, 100, "gen3 daycare mon")
|
|
T.eq(save.hallOfFameTeams[1][1].trainerId, 100, "gen3 hall of fame")
|
|
local b = save.secretBases[1].trainerId
|
|
T.eq(b[1] + b[2] * 256, 100, "own secret base TID")
|
|
T.eq(b[3] + b[4] * 256, save.secretId, "own secret base SID")
|
|
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 } } })
|
|
local gold = SaveData.createSlot("gold")
|
|
SaveData.writeSlot("gold", gold, { version = "gold", generation = 2, player = { id = 222, name = "GOLD" },
|
|
party = { { species = "CYNDAQUIL", otId = 222 }, { species = "ABRA", otId = 111, traded = true } } })
|
|
local em = SaveData.createSlot("emerald")
|
|
SaveData.writeSlot("emerald", em, { version = "emerald", engine = "game3", generation = 3,
|
|
trainerId = 333, secretId = 5, name = "MAY", party = { { otId = 333, otSecretId = 5 } } })
|
|
|
|
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 } } })
|
|
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 } } })
|
|
|
|
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
|
|
|
|
love.filesystem = realFS
|
|
|
|
T.finish("trainer_id_sync")
|