mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 20:50:21 +02:00
+25
-11
@@ -982,7 +982,11 @@ end
|
||||
-- parked the player until every direction was re-pressed (#799).
|
||||
function Game:focus(f)
|
||||
Input:reset()
|
||||
if f then Input:reconcile() end
|
||||
if f then
|
||||
Input:reconcile()
|
||||
local eng = self:syncEngine()
|
||||
if eng then pcall(eng.noteResumed, eng) end
|
||||
end
|
||||
TouchControls:reset()
|
||||
self:cancelPointers()
|
||||
end
|
||||
@@ -1002,6 +1006,8 @@ function Game:onResume()
|
||||
Input:reconcile()
|
||||
TouchControls:reset()
|
||||
self:cancelPointers()
|
||||
local eng = self:syncEngine()
|
||||
if eng then pcall(eng.noteResumed, eng) end
|
||||
-- Chip music may survive NX suspend as a duplicate stream; stop it and let
|
||||
-- the active screen re-cue on the next frame (hardware audio check: T19).
|
||||
-- Desktop/mobile window-visible flips must not kill overworld music.
|
||||
@@ -1209,18 +1215,26 @@ end
|
||||
|
||||
function Game:syncEngine()
|
||||
if self._syncOff then return nil end
|
||||
if self._syncEngineRef then return self._syncEngineRef end
|
||||
local ok, SyncEngine = pcall(require, "src.sync.SyncEngine")
|
||||
if not ok or type(SyncEngine) ~= "table" then
|
||||
self._syncOff = true
|
||||
return nil
|
||||
end
|
||||
local eng = SyncEngine.shared()
|
||||
local eng = self._syncEngineRef
|
||||
if not eng then
|
||||
self._syncOff = true
|
||||
return nil
|
||||
local ok, SyncEngine = pcall(require, "src.sync.SyncEngine")
|
||||
if not ok or type(SyncEngine) ~= "table" then
|
||||
self._syncOff = true
|
||||
return nil
|
||||
end
|
||||
eng = SyncEngine.shared()
|
||||
if not eng then
|
||||
self._syncOff = true
|
||||
return nil
|
||||
end
|
||||
self._syncEngineRef = eng
|
||||
end
|
||||
if type(eng.protectPlaythrough) == "function" then
|
||||
local meta = self.save and self.save.meta
|
||||
eng:protectPlaythrough(
|
||||
(self.save and self.save.version) or require("src.core.GameVersion").get(),
|
||||
type(meta) == "table" and meta.playthroughId or nil)
|
||||
end
|
||||
self._syncEngineRef = eng
|
||||
return eng
|
||||
end
|
||||
|
||||
|
||||
@@ -4554,7 +4554,7 @@ local function buildSyncHome(imp, m, eng)
|
||||
local linked = eng:linked()
|
||||
local codes = eng.codes
|
||||
local body = linked
|
||||
and Strings("This device is linked. Saves and the mod list sync when the launcher opens and a few seconds after each save.")
|
||||
and Strings("This device is linked. Saves sync when the launcher opens, a few seconds after each save, and every few minutes while the app is running.")
|
||||
or Strings(SYNC_HINT)
|
||||
local innerW = w - 2 * pad
|
||||
local hintH = Kit.wrapHeight("small", body, innerW, 5)
|
||||
|
||||
@@ -1537,6 +1537,9 @@ function RomImporter:focus(f)
|
||||
self._modPress = nil
|
||||
return
|
||||
end
|
||||
if type(self._sync) == "table" then
|
||||
pcall(self._sync.noteResumed, self._sync)
|
||||
end
|
||||
if not (f and self.android and self.workState ~= "working") then return end
|
||||
-- SAF create-document finished: GameActivity wrote export_done.flag.
|
||||
if love.filesystem.getInfo("export_done.flag", "file") then
|
||||
|
||||
+25
-2
@@ -7,6 +7,7 @@ SyncEngine.__index = SyncEngine
|
||||
|
||||
SyncEngine.UPLOAD_DEBOUNCE = 5
|
||||
SyncEngine.AUTO_INTERVAL = 300
|
||||
SyncEngine.RESUME_MIN_GAP = 60
|
||||
SyncEngine.MAX_STEPS_PER_UPDATE = 8
|
||||
|
||||
local IDLE_STATUS = "Ready"
|
||||
@@ -133,6 +134,7 @@ function SyncEngine.new(opts)
|
||||
eng.modPlan = nil
|
||||
eng.shareCode = nil
|
||||
eng.clock = 0
|
||||
eng.autoAt = SyncEngine.AUTO_INTERVAL
|
||||
eng.queue = {}
|
||||
eng.pending = nil
|
||||
eng.uploadAt = nil
|
||||
@@ -258,6 +260,11 @@ function SyncEngine:update(dt)
|
||||
self.uploadAt = nil
|
||||
if self.state.enabled and self:linked() then self:syncNow() end
|
||||
end
|
||||
if self.clock >= self.autoAt and not self:busy()
|
||||
and (self.phase == "idle" or self.phase == "error")
|
||||
and self.state.enabled and self:linked() then
|
||||
self:syncNow()
|
||||
end
|
||||
local steps = 0
|
||||
while not self.pending and #self.queue > 0
|
||||
and steps < SyncEngine.MAX_STEPS_PER_UPDATE do
|
||||
@@ -296,6 +303,7 @@ function SyncEngine:createAccount(label)
|
||||
eng.phase = "idle"
|
||||
eng.status = "Sync account created"
|
||||
eng:_persist()
|
||||
eng:syncNow()
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -385,9 +393,24 @@ function SyncEngine:setEnabled(enabled)
|
||||
return self.state.enabled
|
||||
end
|
||||
|
||||
function SyncEngine:protectPlaythrough(version, playthroughId)
|
||||
self.protectedKey = SyncState.key(version, playthroughId)
|
||||
end
|
||||
|
||||
function SyncEngine:noteResumed()
|
||||
if not (self.state.enabled and self:linked()) then return end
|
||||
if self:busy() or self.phase == "conflict" then return end
|
||||
if self.now() - (tonumber(self.state.lastSyncAt) or 0)
|
||||
< SyncEngine.RESUME_MIN_GAP then
|
||||
return
|
||||
end
|
||||
self:syncNow()
|
||||
end
|
||||
|
||||
function SyncEngine:syncNow()
|
||||
if not self:linked() then return false, "this device is not linked" end
|
||||
if self.pending then return false, "sync is busy" end
|
||||
self.autoAt = self.clock + SyncEngine.AUTO_INTERVAL
|
||||
self.queue = {}
|
||||
self.conflicts = {}
|
||||
self.state.pendingConflicts = {}
|
||||
@@ -437,13 +460,13 @@ function SyncEngine:_planFrom(remoteState)
|
||||
self:_addConflict(entry, key, row)
|
||||
elseif localChanged then
|
||||
self:_queueUpload(entry, key, false)
|
||||
elseif remoteChanged then
|
||||
elseif remoteChanged and key ~= self.protectedKey then
|
||||
self:_queueDownload(key, entry.version, entry.playthroughId, "replace")
|
||||
end
|
||||
end
|
||||
end
|
||||
for key, row in pairs(remote) do
|
||||
if not seen[key] then
|
||||
if not seen[key] and key ~= self.protectedKey then
|
||||
local version, id = SyncState.splitKey(key)
|
||||
if version and id then
|
||||
self:_queueDownload(key, version, id, "replace", tonumber(row.rev))
|
||||
|
||||
@@ -94,20 +94,26 @@ do
|
||||
local eng, transport = engine({
|
||||
["POST /sync/create"] = { code = 200, body =
|
||||
'{"account":"aa11","code1":"11112222","code2":"33334444","deviceToken":"tok"}' },
|
||||
}, {}, SyncState.defaults())
|
||||
["GET /sync/state"] = { code = 200, body = '{"saves":{}}' },
|
||||
["PUT /sync/save"] = { code = 200, body = '{"ok":true,"rev":1}' },
|
||||
}, { saveEntry("red", "abc", 500, 400) }, SyncState.defaults())
|
||||
|
||||
T.eq(eng:linked(), false, "a fresh engine is not linked")
|
||||
T.eq(eng.status, "Not set up", "and says so")
|
||||
|
||||
eng:createAccount("laptop")
|
||||
pump(eng, 3)
|
||||
pump(eng)
|
||||
T.eq(eng:linked(), true, "creating an account links this device")
|
||||
T.eq(eng.state.account, "aa11", "and stores the account id")
|
||||
T.eq(eng.codes.code1, "1111-2222", "the first code is shown grouped")
|
||||
T.eq(eng.codes.code2, "3333-4444", "and so is the second")
|
||||
T.eq(eng.state.code1, nil, "codes never enter the persisted state")
|
||||
T.eq(transport.sent[2].url, "http://sync.test/sync/state",
|
||||
"creating the account starts a sync straight away")
|
||||
T.eq(transport.sent[3].method, "PUT",
|
||||
"so the saves that existed before setup are uploaded")
|
||||
T.eq(SyncState.rev(eng.state, "red/abc"), 1, "and the served rev is remembered")
|
||||
T.eq(eng.phase, "idle", "and the engine settles")
|
||||
T.eq(#transport.sent, 1, "one request was made")
|
||||
end
|
||||
|
||||
do
|
||||
@@ -293,14 +299,105 @@ do
|
||||
T.eq(#transport.sent, 0, "with sync off an in-game save uploads nothing")
|
||||
end
|
||||
|
||||
do
|
||||
local eng, transport = engine({
|
||||
["GET /sync/state"] = { code = 200, body = '{"saves":{}}' },
|
||||
}, {})
|
||||
eng:update(SyncEngine.AUTO_INTERVAL - 1)
|
||||
T.eq(#transport.sent, 0, "an idle linked engine does not poll early")
|
||||
eng:update(1)
|
||||
T.eq(#transport.sent, 1, "after the auto interval it checks the server")
|
||||
pump(eng)
|
||||
T.eq(eng.phase, "idle", "and settles")
|
||||
eng:update(SyncEngine.AUTO_INTERVAL - 10)
|
||||
T.eq(#transport.sent, 1, "the next poll waits a whole interval again")
|
||||
eng:update(10)
|
||||
T.eq(#transport.sent, 2, "then fires")
|
||||
end
|
||||
|
||||
do
|
||||
local eng, transport = engine({
|
||||
["GET /sync/state"] = { code = 200, body = '{"saves":{}}' },
|
||||
}, {}, SyncState.defaults())
|
||||
eng:update(SyncEngine.AUTO_INTERVAL * 2)
|
||||
T.eq(#transport.sent, 0, "an unlinked engine never polls on its own")
|
||||
end
|
||||
|
||||
do
|
||||
local calls = 0
|
||||
local eng = engine({
|
||||
["GET /sync/state"] = function()
|
||||
calls = calls + 1
|
||||
if calls == 1 then return { code = 500, body = '{"error":"down"}' } end
|
||||
return { code = 200, body = '{"saves":{}}' }
|
||||
end,
|
||||
}, {})
|
||||
eng:syncNow()
|
||||
pump(eng, 3)
|
||||
T.eq(eng.phase, "error", "the first sync fails")
|
||||
eng:update(SyncEngine.AUTO_INTERVAL)
|
||||
pump(eng, 3)
|
||||
T.eq(eng.phase, "idle", "the auto interval retries and recovers")
|
||||
end
|
||||
|
||||
do
|
||||
local eng, transport = conflictEngine()
|
||||
eng:syncNow()
|
||||
pump(eng)
|
||||
local sent = #transport.sent
|
||||
eng:update(SyncEngine.AUTO_INTERVAL * 2)
|
||||
T.eq(#transport.sent, sent, "a waiting conflict is never auto-synced over")
|
||||
T.eq(eng.phase, "conflict", "the player still decides")
|
||||
end
|
||||
|
||||
do
|
||||
local eng, transport = engine({
|
||||
["GET /sync/state"] = { code = 200, body = '{"saves":{}}' },
|
||||
}, {})
|
||||
eng:noteResumed()
|
||||
T.eq(#transport.sent, 1, "regaining the app checks the server")
|
||||
pump(eng)
|
||||
eng:noteResumed()
|
||||
T.eq(#transport.sent, 1, "but not twice in quick succession")
|
||||
end
|
||||
|
||||
do
|
||||
local eng, transport = engine({}, {}, SyncState.defaults())
|
||||
eng:noteResumed()
|
||||
T.eq(#transport.sent, 0, "an unlinked engine ignores a resume")
|
||||
end
|
||||
|
||||
do
|
||||
local state = linkedState()
|
||||
SyncState.setRev(state, "red/abc", 2, 500)
|
||||
local eng, transport, saves = engine({
|
||||
["GET /sync/state"] = { code = 200,
|
||||
body = '{"saves":{"red/abc":{"rev":4,"meta":{"savedAt":900}},' ..
|
||||
'"gold/xyz":{"rev":1,"meta":{"savedAt":900}}}}' },
|
||||
["GET /sync/save"] = { code = 200,
|
||||
body = '{"rev":1,"meta":{"savedAt":900},"blob":"return { player = {} }"}' },
|
||||
}, { saveEntry("red", "abc", 500, 400) }, state)
|
||||
eng:protectPlaythrough("red", "abc")
|
||||
eng:syncNow()
|
||||
pump(eng)
|
||||
T.eq(#saves.writes, 1, "only the save that is not being played downloads")
|
||||
T.eq(saves.writes[1].version, "gold", "the other playthrough still arrives")
|
||||
T.eq(SyncState.rev(eng.state, "red/abc"), 2,
|
||||
"the live playthrough keeps its rev so the launcher can fetch it later")
|
||||
T.eq(eng.phase, "idle", "and the sync settles")
|
||||
eng:protectPlaythrough("red", nil)
|
||||
T.eq(eng.protectedKey, nil, "no live playthrough means no protection")
|
||||
end
|
||||
|
||||
do
|
||||
local eng = engine({
|
||||
["POST /sync/create"] = { code = 200, body =
|
||||
'{"account":"aa11","code1":"11112222","code2":"33334444",' ..
|
||||
'"deviceToken":"tok","device":"0a1b2c3d"}' },
|
||||
["GET /sync/state"] = { code = 200, body = '{"saves":{}}' },
|
||||
}, {}, SyncState.defaults())
|
||||
eng:createAccount("laptop")
|
||||
pump(eng, 3)
|
||||
pump(eng)
|
||||
T.eq(eng.state.deviceId, "0a1b2c3d",
|
||||
"creating an account records the id the server gave this device")
|
||||
T.eq(SyncState.sanitize(eng.state).deviceId, "0a1b2c3d",
|
||||
|
||||
Reference in New Issue
Block a user