mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Strip downloads by the components the scenario started them with
The instance read its config when the controller answered, so a sync_ field changed during the download could strip a component the scenario still expected, and the scenario treated it as empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aa06b9afe3
commit
45fedf28a0
5 files changed
+26
-18
No files matched your search
@@ -26,15 +26,15 @@ export function enabledComponents(config: lib.InstanceConfig) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Components an upload was serialized with, all of them when not given.
|
||||
* Components the scenario sent with an upload or download, all of them when not given.
|
||||
*
|
||||
* Lua may encode an empty table as an array, which reads as no components.
|
||||
*/
|
||||
export function uploadedComponents(uploaded: Record<string, boolean> | unknown[] | undefined) {
|
||||
if (uploaded === undefined) {
|
||||
export function scenarioComponents(sent: Record<string, boolean> | unknown[] | undefined) {
|
||||
if (sent === undefined) {
|
||||
return componentSet(() => true);
|
||||
}
|
||||
return componentSet(component => !Array.isArray(uploaded) && uploaded[component] === true);
|
||||
return componentSet(component => !Array.isArray(sent) && sent[component] === true);
|
||||
}
|
||||
|
||||
/** Remove the fields of components which are not synced. */
|
||||
@@ -58,7 +58,7 @@ export function stripPlayerData(playerData: IpcPlayerData, enabled: Set<Componen
|
||||
*/
|
||||
export function mergePlayerData(stored: IpcPlayerData | undefined, uploaded: IpcPlayerData) {
|
||||
const { components: uploadComponents, ...merged } = uploaded as IpcPlayerData & Record<string, unknown>;
|
||||
const enabled = uploadedComponents(uploadComponents);
|
||||
const enabled = scenarioComponents(uploadComponents);
|
||||
function keep(keys: readonly string[]) {
|
||||
for (const key of keys) {
|
||||
const value = (stored as Record<string, unknown> | undefined)?.[key];
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { InstancePluginContext } from "@clusterio/host";
|
||||
import {
|
||||
AcquireRequest, AcquireResponse, ReleaseRequest, UploadRequest, DownloadRequest, DownloadResponse, IpcPlayerData,
|
||||
} from "./messages.js";
|
||||
import { enabledComponents, stripPlayerData } from "./components.js";
|
||||
import { enabledComponents, scenarioComponents, stripPlayerData } from "./components.js";
|
||||
|
||||
type IpcPlayerName = {
|
||||
player_name: string
|
||||
@@ -14,6 +14,8 @@ type IpcPlayerName = {
|
||||
type IpcDownloadRequest = {
|
||||
player_name: string,
|
||||
recipe_notifications?: string,
|
||||
// Components the download was started with, all when missing
|
||||
components?: Record<string, boolean> | unknown[],
|
||||
}
|
||||
|
||||
type IpcAcquireResponse = {
|
||||
@@ -199,7 +201,7 @@ export default async function(context: InstancePluginContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
stripPlayerData(response.playerData, enabledComponents(instance.config));
|
||||
stripPlayerData(response.playerData, scenarioComponents(request.components));
|
||||
await applyRecipeNotificationDelta(response.playerData, request.recipe_notifications, logger);
|
||||
|
||||
const chunkSize = instance.config.get("player_sync.rcon_chunk_size");
|
||||
|
||||
@@ -493,6 +493,7 @@ function player_sync.initiate_inventory_download(player, player_record, generati
|
||||
clusterio_api.send_json("player_sync_download", {
|
||||
player_name = player.name,
|
||||
recipe_notifications = recipe_notifications,
|
||||
components = record.components,
|
||||
})
|
||||
|
||||
-- If this is a synced player turn them into a spectator while the
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
components, enabledComponents, mergePlayerData, stripPlayerData, uploadedComponents,
|
||||
components, enabledComponents, mergePlayerData, stripPlayerData, scenarioComponents,
|
||||
} from "../dist/node/components.js";
|
||||
|
||||
const allComponents = Object.keys(components);
|
||||
@@ -79,19 +79,19 @@ describe("player_sync components", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("uploadedComponents()", function() {
|
||||
it("should sync everything when the upload has no components", function() {
|
||||
assert.deepEqual([...uploadedComponents(undefined)], allComponents);
|
||||
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 = uploadedComponents({ force: true, quick_bar: true, settings: false });
|
||||
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(uploadedComponents([]).size, 0);
|
||||
assert.equal(scenarioComponents([]).size, 0);
|
||||
});
|
||||
it("should sync the controller without the inventories", function() {
|
||||
assert.deepEqual([...uploadedComponents({ controller: true })], ["controller"]);
|
||||
assert.deepEqual([...scenarioComponents({ controller: true })], ["controller"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -129,14 +129,19 @@ describe("player_sync", function() {
|
||||
const expected = { generation: 1, name: "test", recipe_notifications: await encode({ add: ["a"] }) };
|
||||
assert.deepEqual(instance.server.rconCommands, [downloadCommand(expected)]);
|
||||
});
|
||||
it("should leave out components the instance does not sync", async function() {
|
||||
instance.mockConfigEntries.set("player_sync.sync_force", false);
|
||||
instance.mockConfigEntries.set("player_sync.sync_recipe_notifications", false);
|
||||
it("should leave out components the download was not started with", async function() {
|
||||
respondWith({ generation: 1, name: "test", force: "enemy", tag: "tag", recipe_notifications: "x" });
|
||||
await download({ player_name: "test" });
|
||||
await download({ player_name: "test", components: { appearance: true } });
|
||||
const expected = { generation: 1, name: "test", tag: "tag" };
|
||||
assert.deepEqual(instance.server.rconCommands, [downloadCommand(expected)]);
|
||||
});
|
||||
it("should ignore config changes made after the download was started", async function() {
|
||||
instance.mockConfigEntries.set("player_sync.sync_force", false);
|
||||
respondWith({ generation: 1, name: "test", force: "enemy", tag: "tag" });
|
||||
await download({ player_name: "test", components: { force: true } });
|
||||
const expected = { generation: 1, name: "test", force: "enemy" };
|
||||
assert.deepEqual(instance.server.rconCommands, [downloadCommand(expected)]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("set_components", function() {
|
||||
|
||||
Reference in new issue
Block a user