diff --git a/plugins/player_sync/messages.ts b/plugins/player_sync/messages.ts index 506b46df..ea6059ee 100644 --- a/plugins/player_sync/messages.ts +++ b/plugins/player_sync/messages.ts @@ -269,12 +269,13 @@ export class UploadResponse { ) { } - static jsonSchema = Type.Object({ - "stored": Type.Boolean(), + // No type: a controller from before this response replies with nothing, which counts as stored + static jsonSchema = Type.Unsafe<{ stored: boolean } | undefined>({ + properties: { "stored": { type: "boolean" } }, }); static fromJSON(json: Static) { - return new this(json.stored); + return new this(json?.stored ?? true); } } diff --git a/plugins/player_sync/module/player_sync.lua b/plugins/player_sync/module/player_sync.lua index 8c27fd02..472f7958 100644 --- a/plugins/player_sync/module/player_sync.lua +++ b/plugins/player_sync/module/player_sync.lua @@ -735,10 +735,15 @@ end -- too, and then downloads what this save sent. function player_sync.reject_upload(player_name, generation) local player = game.get_player(player_name) - if not player or player.connected then + if not player then return end local script_data = get_script_data() + -- A player who is back already still waits for the acquire reply, which the controller sent after this one, and + -- that join then downloads. Past that reply the join has decided and this is not about the copy here any more. + if player.connected and not script_data.players_waiting_for_acquire[player_name] then + return + end local player_record = script_data.players[player.name] if not player_record or player_record.generation ~= generation then return diff --git a/plugins/player_sync/test/plugin.js b/plugins/player_sync/test/plugin.js index 2bc67904..7284404d 100644 --- a/plugins/player_sync/test/plugin.js +++ b/plugins/player_sync/test/plugin.js @@ -60,6 +60,14 @@ describe("player_sync plugin", function() { assert(!validate({ instanceId: 1, playerName: "player", playerData: data }), `accepted ${deleted}`); } }); + it("should take the empty reply of an older controller as a stored upload", function() { + const validate = lib.compile(msg.UploadResponse.jsonSchema); + assert(validate(undefined)); + assert.equal(msg.UploadResponse.fromJSON(undefined).stored, true); + assert(validate({ stored: false })); + assert.equal(msg.UploadResponse.fromJSON({ stored: false }).stored, false); + assert(!validate({ stored: "no" })); + }); describe("SetPlayerDataRequest schema", function() { const validate = lib.compile(msg.SetPlayerDataRequest.jsonSchema); // Shaped like module/serialize.lua output