mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
The controller merged uploads using the uploading instance's current config. An upload serialized under different settings, for example one sent from the save right after an admin turned on sync_inventories and restarted the instance, was then merged as if it held a character and wiped the stored one. Uploads now carry the components they were serialized with, the controller merges by those and drops the key before storing. The scenario keeps the components a player's synced session started with and serializes their upload with them, so changing the options while a player is online takes effect the next time they join. A player whose session lacks a component that is now synced downloads again even if the generation matches. Downloads snapshot the components when they start, so a change during a download can't mix two sets. A failed acquire unsyncs the player again regardless of the inventories setting, as before this branch. The god and dead player inventories move from the inventories component to the controller one, matching what the scenario syncs, which removes the special case in mergePlayerData. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
3.7 KiB
JavaScript
93 lines
3.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
import * as lib from "@clusterio/lib";
|
|
import * as mock from "../../../test/mock.js";
|
|
import controllerEntrypoint from "../dist/node/controller.js";
|
|
import { components as allComponents } from "../dist/node/components.js";
|
|
import { plugin as info } from "../dist/node/index.js";
|
|
import { DatabaseStatsRequest, DownloadRequest, UploadRequest } from "../dist/node/messages.js";
|
|
|
|
describe("player_sync controller", function() {
|
|
const databaseDir = path.join("temp", "test", "player_sync");
|
|
|
|
before(function() {
|
|
try {
|
|
lib.registerPluginMessages([info]);
|
|
} catch (err) {
|
|
// Already registered by the full test suite
|
|
}
|
|
});
|
|
beforeEach(async function() {
|
|
await fs.rm(databaseDir, { recursive: true, force: true });
|
|
await fs.mkdir(databaseDir, { recursive: true });
|
|
});
|
|
|
|
async function load() {
|
|
return await mock.loadControllerPlugin(async (context) => {
|
|
context.controller.mockConfigEntries.set("controller.database_directory", databaseDir);
|
|
await controllerEntrypoint(context);
|
|
}, info);
|
|
}
|
|
|
|
it("should move inventories.json to player_sync.json", async function() { // Alpha 28
|
|
const playerData = { name: "test", generation: 1 };
|
|
await fs.writeFile(path.join(databaseDir, "inventories.json"), JSON.stringify([["test", playerData]]));
|
|
|
|
const controller = await load();
|
|
const stats = await mock.getHandler(controller, DatabaseStatsRequest)();
|
|
assert.equal(stats.databaseEntries, 1);
|
|
assert.equal(stats.largestEntry.name, "test");
|
|
await assert.rejects(fs.access(path.join(databaseDir, "inventories.json")), { code: "ENOENT" });
|
|
await fs.access(path.join(databaseDir, "player_sync.json"));
|
|
});
|
|
it("should not replace an existing player_sync.json", async function() {
|
|
await fs.writeFile(path.join(databaseDir, "inventories.json"), JSON.stringify([["old", { name: "old" }]]));
|
|
await fs.writeFile(path.join(databaseDir, "player_sync.json"), JSON.stringify([["new", { name: "new" }]]));
|
|
|
|
const controller = await load();
|
|
const stats = await mock.getHandler(controller, DatabaseStatsRequest)();
|
|
assert.equal(stats.databaseEntries, 1);
|
|
assert.equal(stats.largestEntry.name, "new");
|
|
});
|
|
|
|
describe("UploadRequest", function() {
|
|
let controller;
|
|
let disabled;
|
|
beforeEach(async function() {
|
|
disabled = [];
|
|
const stored = { generation: 1, name: "test", force: "player", tag: "stored" };
|
|
await fs.writeFile(path.join(databaseDir, "player_sync.json"), JSON.stringify([["test", stored]]));
|
|
controller = await load();
|
|
controller.instances = new Map([[1, {
|
|
config: {
|
|
get: name => {
|
|
if (name === "instance.name") { return "test instance"; }
|
|
return !disabled.includes(name.replace("player_sync.sync_", ""));
|
|
},
|
|
},
|
|
}]]);
|
|
});
|
|
|
|
async function upload(playerData) {
|
|
await mock.getHandler(controller, UploadRequest)({ instanceId: 1, playerName: "test", playerData });
|
|
return (await mock.getHandler(controller, DownloadRequest)({ instanceId: 1, playerName: "test" }))
|
|
.playerData;
|
|
}
|
|
|
|
it("should store the upload as is when it has no components", async function() {
|
|
const playerData = { generation: 2, name: "test", force: "enemy", tag: "uploaded" };
|
|
assert.deepEqual(await upload(playerData), playerData);
|
|
});
|
|
it("should merge by the payload's components and ignore the instance config", async function() {
|
|
disabled = ["appearance"];
|
|
const components = Object.fromEntries(Object.keys(allComponents).map(c => [c, c !== "force"]));
|
|
assert.deepEqual(
|
|
await upload({ generation: 2, name: "test", tag: "uploaded", components }),
|
|
{ generation: 2, name: "test", force: "player", tag: "uploaded" },
|
|
);
|
|
});
|
|
});
|
|
});
|