mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 07:04:53 +09:00
Syncing the controller required syncing the inventories, and with the controller off a god or ghost kept its local items while the stored character was left alone. Cooldude pointed out there is no reason the items can't follow the player to a different controller. The controller and inventories components are now independent. The god inventory and the hidden ghost inventory belong to the inventories component. On download the scenario first settles the controller, synced or local, and then places the stored items into whatever that controller holds. An inventory goes slot for slot into the inventory of the same name when it fits, otherwise its items are inserted into the main, guns, ammo and armor inventories, and anything left goes into a corpse below the player. A spectator holds nothing, so everything goes into the corpse. A player without a character gets the crafting queue ingredients as items. This removes the early respawn of a ghost with a stored character and the upload of the respawned character, a ghost now gets and uploads its hidden inventory. Download spectates every synced player when the inventories are synced, as a god or ghost now holds synced items too. The controller keeps the stored logistic requests when the upload came from a player with nowhere to hold them. Slot filters are dropped when the destination does not support them, such as a script inventory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
153 lines
6.1 KiB
JavaScript
153 lines
6.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
components, enabledComponents, mergePlayerData, stripPlayerData, uploadedComponents,
|
|
} 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 sync the controller without the inventories", function() {
|
|
const enabled = enabledComponents(configWith(["inventories"]));
|
|
assert.equal(enabled.has("controller"), true);
|
|
});
|
|
});
|
|
|
|
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");
|
|
});
|
|
it("should remove the god inventory with the inventories", function() {
|
|
const playerData = structuredClone(stored);
|
|
stripPlayerData(playerData, new Set(allComponents.filter(c => c !== "inventories")));
|
|
assert.equal("inventories" in playerData, false);
|
|
assert.equal(playerData.controller, "god");
|
|
});
|
|
});
|
|
|
|
describe("uploadedComponents()", function() {
|
|
it("should sync everything when the upload has no components", function() {
|
|
assert.deepEqual([...uploadedComponents(undefined)], allComponents);
|
|
});
|
|
it("should sync the components marked true", function() {
|
|
const enabled = uploadedComponents({ force: true, quick_bar: true, settings: false });
|
|
assert.deepEqual([...enabled], ["force", "quick_bar"]);
|
|
});
|
|
it("should sync nothing for an empty table encoded as an array", function() {
|
|
assert.equal(uploadedComponents([]).size, 0);
|
|
});
|
|
it("should sync the controller without the inventories", function() {
|
|
assert.deepEqual([...uploadedComponents({ controller: true })], ["controller"]);
|
|
});
|
|
});
|
|
|
|
describe("mergePlayerData()", function() {
|
|
function withComponents(data, disabled) {
|
|
const enabled = allComponents.filter(c => !disabled.includes(c));
|
|
return { ...data, components: Object.fromEntries(enabled.map(c => [c, true])) };
|
|
}
|
|
|
|
it("should store the upload as is when it has no components", function() {
|
|
assert.deepEqual(mergePlayerData(stored, uploaded), uploaded);
|
|
});
|
|
it("should not store the components of the upload", function() {
|
|
assert.deepEqual(mergePlayerData(stored, withComponents(uploaded, [])), uploaded);
|
|
});
|
|
it("should keep the stored fields of disabled components", function() {
|
|
const merged = mergePlayerData(stored, withComponents(uploaded, ["force", "appearance"]));
|
|
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, withComponents(uploaded, ["force"]));
|
|
assert.equal("force" in merged, false);
|
|
assert.equal(merged.tag, "uploaded");
|
|
});
|
|
it("should take the items but not the controller when the controller is not synced", function() {
|
|
const merged = mergePlayerData(stored, withComponents(uploaded, ["controller"]));
|
|
assert.equal(merged.controller, "god");
|
|
assert.equal(merged.cheat_mode, true);
|
|
assert.equal(merged.character, character);
|
|
assert.equal("inventories" in merged, false, "the god inventory moved to the character");
|
|
});
|
|
it("should take the items of a player without a character", function() {
|
|
const storedCharacter = {
|
|
...stored, character, crafting_queue: ["stored"], personal_logistic_slots: [{ name: "stored" }],
|
|
};
|
|
const god = { ...uploaded, controller: "god", inventories: { main: ["new god item"] } };
|
|
delete god.character;
|
|
delete god.crafting_queue;
|
|
delete god.personal_logistic_slots;
|
|
const merged = mergePlayerData(storedCharacter, withComponents(god, ["controller"]));
|
|
assert.equal("character" in merged, false);
|
|
assert.equal("crafting_queue" in merged, false);
|
|
assert.deepEqual(merged.inventories, { main: ["new god item"] });
|
|
assert.deepEqual(merged.personal_logistic_slots, [{ name: "stored" }], "nowhere to hold logistics");
|
|
});
|
|
it("should drop logistic requests a character uploaded without", function() {
|
|
const storedSlots = { ...stored, personal_logistic_slots: [{ name: "stored" }] };
|
|
const noSlots = { ...uploaded };
|
|
delete noSlots.personal_logistic_slots;
|
|
const merged = mergePlayerData(storedSlots, withComponents(noSlots, []));
|
|
assert.equal("personal_logistic_slots" in merged, false);
|
|
});
|
|
});
|
|
});
|