mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 05:00:43 +02:00
+17
-3
@@ -982,7 +982,11 @@ end
|
|||||||
-- parked the player until every direction was re-pressed (#799).
|
-- parked the player until every direction was re-pressed (#799).
|
||||||
function Game:focus(f)
|
function Game:focus(f)
|
||||||
Input:reset()
|
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()
|
TouchControls:reset()
|
||||||
self:cancelPointers()
|
self:cancelPointers()
|
||||||
end
|
end
|
||||||
@@ -1002,6 +1006,8 @@ function Game:onResume()
|
|||||||
Input:reconcile()
|
Input:reconcile()
|
||||||
TouchControls:reset()
|
TouchControls:reset()
|
||||||
self:cancelPointers()
|
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
|
-- 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).
|
-- the active screen re-cue on the next frame (hardware audio check: T19).
|
||||||
-- Desktop/mobile window-visible flips must not kill overworld music.
|
-- Desktop/mobile window-visible flips must not kill overworld music.
|
||||||
@@ -1209,18 +1215,26 @@ end
|
|||||||
|
|
||||||
function Game:syncEngine()
|
function Game:syncEngine()
|
||||||
if self._syncOff then return nil end
|
if self._syncOff then return nil end
|
||||||
if self._syncEngineRef then return self._syncEngineRef end
|
local eng = self._syncEngineRef
|
||||||
|
if not eng then
|
||||||
local ok, SyncEngine = pcall(require, "src.sync.SyncEngine")
|
local ok, SyncEngine = pcall(require, "src.sync.SyncEngine")
|
||||||
if not ok or type(SyncEngine) ~= "table" then
|
if not ok or type(SyncEngine) ~= "table" then
|
||||||
self._syncOff = true
|
self._syncOff = true
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
local eng = SyncEngine.shared()
|
eng = SyncEngine.shared()
|
||||||
if not eng then
|
if not eng then
|
||||||
self._syncOff = true
|
self._syncOff = true
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
self._syncEngineRef = eng
|
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
|
||||||
return eng
|
return eng
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -4554,7 +4554,7 @@ local function buildSyncHome(imp, m, eng)
|
|||||||
local linked = eng:linked()
|
local linked = eng:linked()
|
||||||
local codes = eng.codes
|
local codes = eng.codes
|
||||||
local body = linked
|
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)
|
or Strings(SYNC_HINT)
|
||||||
local innerW = w - 2 * pad
|
local innerW = w - 2 * pad
|
||||||
local hintH = Kit.wrapHeight("small", body, innerW, 5)
|
local hintH = Kit.wrapHeight("small", body, innerW, 5)
|
||||||
|
|||||||
@@ -1537,6 +1537,9 @@ function RomImporter:focus(f)
|
|||||||
self._modPress = nil
|
self._modPress = nil
|
||||||
return
|
return
|
||||||
end
|
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
|
if not (f and self.android and self.workState ~= "working") then return end
|
||||||
-- SAF create-document finished: GameActivity wrote export_done.flag.
|
-- SAF create-document finished: GameActivity wrote export_done.flag.
|
||||||
if love.filesystem.getInfo("export_done.flag", "file") then
|
if love.filesystem.getInfo("export_done.flag", "file") then
|
||||||
|
|||||||
+25
-2
@@ -7,6 +7,7 @@ SyncEngine.__index = SyncEngine
|
|||||||
|
|
||||||
SyncEngine.UPLOAD_DEBOUNCE = 5
|
SyncEngine.UPLOAD_DEBOUNCE = 5
|
||||||
SyncEngine.AUTO_INTERVAL = 300
|
SyncEngine.AUTO_INTERVAL = 300
|
||||||
|
SyncEngine.RESUME_MIN_GAP = 60
|
||||||
SyncEngine.MAX_STEPS_PER_UPDATE = 8
|
SyncEngine.MAX_STEPS_PER_UPDATE = 8
|
||||||
|
|
||||||
local IDLE_STATUS = "Ready"
|
local IDLE_STATUS = "Ready"
|
||||||
@@ -133,6 +134,7 @@ function SyncEngine.new(opts)
|
|||||||
eng.modPlan = nil
|
eng.modPlan = nil
|
||||||
eng.shareCode = nil
|
eng.shareCode = nil
|
||||||
eng.clock = 0
|
eng.clock = 0
|
||||||
|
eng.autoAt = SyncEngine.AUTO_INTERVAL
|
||||||
eng.queue = {}
|
eng.queue = {}
|
||||||
eng.pending = nil
|
eng.pending = nil
|
||||||
eng.uploadAt = nil
|
eng.uploadAt = nil
|
||||||
@@ -258,6 +260,11 @@ function SyncEngine:update(dt)
|
|||||||
self.uploadAt = nil
|
self.uploadAt = nil
|
||||||
if self.state.enabled and self:linked() then self:syncNow() end
|
if self.state.enabled and self:linked() then self:syncNow() end
|
||||||
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
|
local steps = 0
|
||||||
while not self.pending and #self.queue > 0
|
while not self.pending and #self.queue > 0
|
||||||
and steps < SyncEngine.MAX_STEPS_PER_UPDATE do
|
and steps < SyncEngine.MAX_STEPS_PER_UPDATE do
|
||||||
@@ -296,6 +303,7 @@ function SyncEngine:createAccount(label)
|
|||||||
eng.phase = "idle"
|
eng.phase = "idle"
|
||||||
eng.status = "Sync account created"
|
eng.status = "Sync account created"
|
||||||
eng:_persist()
|
eng:_persist()
|
||||||
|
eng:syncNow()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -385,9 +393,24 @@ function SyncEngine:setEnabled(enabled)
|
|||||||
return self.state.enabled
|
return self.state.enabled
|
||||||
end
|
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()
|
function SyncEngine:syncNow()
|
||||||
if not self:linked() then return false, "this device is not linked" end
|
if not self:linked() then return false, "this device is not linked" end
|
||||||
if self.pending then return false, "sync is busy" end
|
if self.pending then return false, "sync is busy" end
|
||||||
|
self.autoAt = self.clock + SyncEngine.AUTO_INTERVAL
|
||||||
self.queue = {}
|
self.queue = {}
|
||||||
self.conflicts = {}
|
self.conflicts = {}
|
||||||
self.state.pendingConflicts = {}
|
self.state.pendingConflicts = {}
|
||||||
@@ -437,13 +460,13 @@ function SyncEngine:_planFrom(remoteState)
|
|||||||
self:_addConflict(entry, key, row)
|
self:_addConflict(entry, key, row)
|
||||||
elseif localChanged then
|
elseif localChanged then
|
||||||
self:_queueUpload(entry, key, false)
|
self:_queueUpload(entry, key, false)
|
||||||
elseif remoteChanged then
|
elseif remoteChanged and key ~= self.protectedKey then
|
||||||
self:_queueDownload(key, entry.version, entry.playthroughId, "replace")
|
self:_queueDownload(key, entry.version, entry.playthroughId, "replace")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for key, row in pairs(remote) do
|
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)
|
local version, id = SyncState.splitKey(key)
|
||||||
if version and id then
|
if version and id then
|
||||||
self:_queueDownload(key, version, id, "replace", tonumber(row.rev))
|
self:_queueDownload(key, version, id, "replace", tonumber(row.rev))
|
||||||
|
|||||||
@@ -94,20 +94,26 @@ do
|
|||||||
local eng, transport = engine({
|
local eng, transport = engine({
|
||||||
["POST /sync/create"] = { code = 200, body =
|
["POST /sync/create"] = { code = 200, body =
|
||||||
'{"account":"aa11","code1":"11112222","code2":"33334444","deviceToken":"tok"}' },
|
'{"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:linked(), false, "a fresh engine is not linked")
|
||||||
T.eq(eng.status, "Not set up", "and says so")
|
T.eq(eng.status, "Not set up", "and says so")
|
||||||
|
|
||||||
eng:createAccount("laptop")
|
eng:createAccount("laptop")
|
||||||
pump(eng, 3)
|
pump(eng)
|
||||||
T.eq(eng:linked(), true, "creating an account links this device")
|
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.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.code1, "1111-2222", "the first code is shown grouped")
|
||||||
T.eq(eng.codes.code2, "3333-4444", "and so is the second")
|
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(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(eng.phase, "idle", "and the engine settles")
|
||||||
T.eq(#transport.sent, 1, "one request was made")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
do
|
do
|
||||||
@@ -293,14 +299,105 @@ do
|
|||||||
T.eq(#transport.sent, 0, "with sync off an in-game save uploads nothing")
|
T.eq(#transport.sent, 0, "with sync off an in-game save uploads nothing")
|
||||||
end
|
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
|
do
|
||||||
local eng = engine({
|
local eng = engine({
|
||||||
["POST /sync/create"] = { code = 200, body =
|
["POST /sync/create"] = { code = 200, body =
|
||||||
'{"account":"aa11","code1":"11112222","code2":"33334444",' ..
|
'{"account":"aa11","code1":"11112222","code2":"33334444",' ..
|
||||||
'"deviceToken":"tok","device":"0a1b2c3d"}' },
|
'"deviceToken":"tok","device":"0a1b2c3d"}' },
|
||||||
|
["GET /sync/state"] = { code = 200, body = '{"saves":{}}' },
|
||||||
}, {}, SyncState.defaults())
|
}, {}, SyncState.defaults())
|
||||||
eng:createAccount("laptop")
|
eng:createAccount("laptop")
|
||||||
pump(eng, 3)
|
pump(eng)
|
||||||
T.eq(eng.state.deviceId, "0a1b2c3d",
|
T.eq(eng.state.deviceId, "0a1b2c3d",
|
||||||
"creating an account records the id the server gave this device")
|
"creating an account records the id the server gave this device")
|
||||||
T.eq(SyncState.sanitize(eng.state).deviceId, "0a1b2c3d",
|
T.eq(SyncState.sanitize(eng.state).deviceId, "0a1b2c3d",
|
||||||
|
|||||||
Reference in New Issue
Block a user