mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 23:14:53 +09:00
Adds an inventory_sync.sync_<component> instance config field for the controller, force, appearance, inventories, logistics, quick bar, settings and recipe notifications. All default to on, which keeps the current behaviour. A component which is off on an instance is left alone there and its fields are not stored from that instance. The controller merges each upload with the stored data using the uploading instance's config, the instance strips the fields from downloads before sending them over RCON, and the scenario is told the enabled set on start and when it changes so it can skip serializing them and skip the steps which replace the character. The controller is only synced together with the inventories, as switching controller can destroy the character holding them. Without the controller synced the player gets back the controller they had on the instance, only character inventories move between instances, and a player without a character keeps their inventory. Closes #968 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
4.4 KiB
JavaScript
113 lines
4.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
|
|
import { components, enabledComponents, mergePlayerData, stripPlayerData } from "../dist/node/components.js";
|
|
|
|
const allComponents = Object.keys(components);
|
|
|
|
function configWith(disabled = []) {
|
|
return { get: name => !disabled.includes(name.replace("player_sync.sync_", "")) };
|
|
}
|
|
|
|
const character = { inventories: { main: [] } };
|
|
const stored = {
|
|
generation: 1,
|
|
name: "test",
|
|
controller: "god",
|
|
cheat_mode: true,
|
|
force: "player",
|
|
color: [1, 0, 0],
|
|
chat_color: [1, 0, 0],
|
|
tag: "stored",
|
|
inventories: { main: ["stored god item"] },
|
|
quick_bar: ["stored-item"],
|
|
recipe_notifications: "stored",
|
|
};
|
|
const uploaded = {
|
|
generation: 2,
|
|
name: "test",
|
|
controller: "character",
|
|
cheat_mode: false,
|
|
force: "enemy",
|
|
color: [0, 1, 0],
|
|
chat_color: [0, 1, 0],
|
|
tag: "uploaded",
|
|
character,
|
|
crafting_queue: [],
|
|
personal_logistic_slots: [],
|
|
quick_bar: ["uploaded-item"],
|
|
recipe_notifications: "uploaded",
|
|
};
|
|
|
|
describe("player_sync components", function() {
|
|
describe("enabledComponents()", function() {
|
|
it("should enable every component by default", function() {
|
|
assert.deepEqual([...enabledComponents(configWith())], allComponents);
|
|
});
|
|
it("should leave out disabled components", function() {
|
|
const enabled = enabledComponents(configWith(["force", "quick_bar"]));
|
|
assert.equal(enabled.has("force"), false);
|
|
assert.equal(enabled.has("quick_bar"), false);
|
|
assert.equal(enabled.size, allComponents.length - 2);
|
|
});
|
|
it("should not sync the controller without the inventories", function() {
|
|
const enabled = enabledComponents(configWith(["inventories"]));
|
|
assert.equal(enabled.has("controller"), false);
|
|
});
|
|
});
|
|
|
|
describe("stripPlayerData()", function() {
|
|
it("should keep everything when all components are synced", function() {
|
|
const playerData = structuredClone(uploaded);
|
|
stripPlayerData(playerData, new Set(allComponents));
|
|
assert.deepEqual(playerData, uploaded);
|
|
});
|
|
it("should remove the fields of disabled components", function() {
|
|
const playerData = structuredClone(uploaded);
|
|
stripPlayerData(playerData, new Set(allComponents.filter(c => !["appearance", "inventories"].includes(c))));
|
|
for (const key of ["color", "chat_color", "tag", "character", "crafting_queue"]) {
|
|
assert.equal(key in playerData, false, key);
|
|
}
|
|
assert.equal(playerData.force, "enemy");
|
|
});
|
|
});
|
|
|
|
describe("mergePlayerData()", function() {
|
|
it("should store the upload as is when all components are synced", function() {
|
|
assert.deepEqual(mergePlayerData(stored, uploaded, new Set(allComponents)), uploaded);
|
|
});
|
|
it("should keep the stored fields of disabled components", function() {
|
|
const enabled = new Set(allComponents.filter(c => !["force", "appearance"].includes(c)));
|
|
const merged = mergePlayerData(stored, uploaded, enabled);
|
|
assert.equal(merged.force, "player");
|
|
assert.deepEqual(merged.color, [1, 0, 0]);
|
|
assert.equal(merged.tag, "stored");
|
|
assert.deepEqual(merged.quick_bar, ["uploaded-item"]);
|
|
assert.equal(merged.generation, 2);
|
|
});
|
|
it("should drop disabled components without stored data", function() {
|
|
const merged = mergePlayerData(undefined, uploaded, new Set(allComponents.filter(c => c !== "force")));
|
|
assert.equal("force" in merged, false);
|
|
assert.equal(merged.tag, "uploaded");
|
|
});
|
|
it("should take the character but not the controller when the controller is not synced", function() {
|
|
const merged = mergePlayerData(stored, uploaded, new Set(allComponents.filter(c => c !== "controller")));
|
|
assert.equal(merged.controller, "god");
|
|
assert.equal(merged.cheat_mode, true);
|
|
assert.equal(merged.character, character);
|
|
assert.deepEqual(merged.inventories, { main: ["stored god item"] }, "god inventory is kept");
|
|
});
|
|
it("should keep the character data for a player without one when the controller is not synced", function() {
|
|
const storedCharacter = { ...stored, character, crafting_queue: ["stored"], personal_logistic_slots: [] };
|
|
const god = { ...uploaded, controller: "god", inventories: { main: ["new god item"] } };
|
|
delete god.character;
|
|
delete god.crafting_queue;
|
|
delete god.personal_logistic_slots;
|
|
const enabled = new Set(allComponents.filter(c => c !== "controller"));
|
|
const merged = mergePlayerData(storedCharacter, god, enabled);
|
|
assert.equal(merged.character, character);
|
|
assert.deepEqual(merged.crafting_queue, ["stored"]);
|
|
assert.deepEqual(merged.inventories, { main: ["stored god item"] });
|
|
});
|
|
});
|
|
});
|