Close the remaining ways an accepted import fails to restore

Imports no longer accept the cutscene controller, which needs waypoints
to restore. Logistic filters and the basic item stack fields now have the
types and limits Factorio enforces, and the inventory summary skips
malformed items and counts repeated exported items once instead of
expanding them.

Lua's JSON loses precision past 2^50, so a generation there could no
longer be counted up and uploads were discarded. Imports now start over
from generation 1 when the stored or imported generation is at that limit.
Instances holding a copy from another epoch take the stored generation
instead of keeping their own higher one when there is no data to download.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 5.5 committed 2026-10-19 19:50:37 +00:00
1 parent 9ecd497f1e
commit 39df22be65
6 files changed
+167 -20

No files matched your search

+46 -2
View File
@@ -115,7 +115,7 @@ describe("player_sync controller", function() {
return await handler(msg.AcquireRequest)(new msg.AcquireRequest(instanceId, playerName));
}
beforeEach(async function() {
async function loadWithInstances() {
controller = await mock.loadControllerPlugin(async (context) => {
context.controller.mockConfigEntries.set("controller.database_directory", databaseDir);
// Negative so locks expire as soon as their instance stops
@@ -127,6 +127,10 @@ describe("player_sync controller", function() {
new lib.InstanceConfig("controller", { "instance.id": id, "instance.name": name }), status,
));
}
}
beforeEach(async function() {
await loadWithInstances();
await handler(msg.UploadRequest)(new msg.UploadRequest(1, "stored", playerData("stored", 3)));
assert(await acquire(1, "stored"));
@@ -220,7 +224,19 @@ describe("player_sync controller", function() {
await setJSON("stored", playerData("stored", 0));
const before = (await saved()).get("stored");
assert.equal(before.epoch, 1);
for (const extra of [{ crafting_queue: {} }, { deleted: false }, { deleted: true }]) {
const badFilter = { value: { name: "iron-plate" }, min: "many" };
for (const extra of [
{ crafting_queue: {} },
{ deleted: false },
{ deleted: true },
{ controller: "cutscene" },
{ inventories: { main: { i: [{ e: null }] } } },
{ personal_logistic_slots: {
enabled: true,
trash_not_requested: false,
sections: [{ group: "", active: true, multiplier: 1, filters: [badFilter] }],
} },
]) {
await assert.rejects(
setJSON("stored", playerData("stored", 0, extra)),
{ message: "Request SetPlayerDataRequest failed validation" },
@@ -228,6 +244,34 @@ describe("player_sync controller", function() {
}
assert.deepEqual((await saved()).get("stored"), before);
});
it("should start over from imported generations past the limit", async function() {
await release("stored");
assert.equal((await setJSON("stored", playerData("stored", 2 ** 53))).generation, 4);
await upload(1, playerData("stored", 5, { epoch: 1 }));
assert.deepEqual((await saved()).get("stored"), playerData("stored", 5, { epoch: 1 }));
});
for (const deleted of [false, true]) {
it(`should recover ${deleted ? "deleted" : "stored"} data past the generation limit`, async function() {
// Could be stored by uploads or by imports from before the limit
for (const generation of [msg.generationLimit, 2 ** 53]) {
const stuck = { ...playerData("stored", generation), ...deleted ? { deleted } : {} };
const entries = [["stored", stuck]];
await fs.writeFile(path.join(databaseDir, "player_sync.json"), JSON.stringify(entries));
await loadWithInstances();
// Importing the exported data as is
assert.equal((await setJSON("stored", playerData("stored", generation))).generation, 1);
await upload(1, playerData("stored", 2, { epoch: 1 }));
assert.deepEqual((await saved()).get("stored"), playerData("stored", 2, { epoch: 1 }));
}
});
}
it("should keep counting up below the generation limit", async function() {
const stored = playerData("stored", msg.generationLimit - 1);
await fs.writeFile(path.join(databaseDir, "player_sync.json"), JSON.stringify([["stored", stored]]));
await loadWithInstances();
const response = await setJSON("stored", playerData("stored", 0));
assert.equal(response.generation, msg.generationLimit);
});
it("should only load entries marked deleted: true as tombstones", async function() {
// Imports from before the marker was reserved could store deleted: false
const data = playerData("stored", 4, { epoch: 1 });