import assert from "node:assert/strict"; import { components, enabledComponents, mergePlayerData, stripPlayerData, scenarioComponents, } 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("scenarioComponents()", function() { it("should sync everything when no components are sent", function() { assert.deepEqual([...scenarioComponents(undefined)], allComponents); }); it("should sync the components marked true", function() { const enabled = scenarioComponents({ 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(scenarioComponents([]).size, 0); }); it("should sync the controller without the inventories", function() { assert.deepEqual([...scenarioComponents({ 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); }); it("should take cleared logistic requests from an upload without inventories", function() { const storedSlots = { ...stored, personal_logistic_slots: [{ name: "stored" }] }; // Factorio 1.1 serializes a character without requests as an empty table const cleared = { ...uploaded, personal_logistic_slots: {} }; delete cleared.character; delete cleared.crafting_queue; const merged = mergePlayerData(storedSlots, withComponents(cleared, ["inventories"])); assert.deepEqual(merged.personal_logistic_slots, {}); }); }); });