Files
factorio-clusterio/plugins/player_sync/components.ts
T
bbassieandClaude Opus 5.5 0e559cf000 Allow inventory sync components to be turned off per instance
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>
2026-10-17 15:25:06 +00:00

87 lines
2.7 KiB
TypeScript

import type * as lib from "@clusterio/lib";
import type { IpcPlayerData } from "./messages.js";
/** Parts of the player data an instance can choose to sync, and the fields of the player data holding them. */
export const components = {
controller: ["controller", "ticks_to_respawn", "cheat_mode"],
force: ["force"],
appearance: ["color", "chat_color", "tag"],
inventories: ["character", "inventories", "crafting_queue"],
logistics: ["personal_logistic_slots"],
quick_bar: ["quick_bar", "hotbar"],
settings: ["flashlight", "shortcuts", "game_view_settings"],
recipe_notifications: ["recipe_notifications"],
} as const;
export type Component = keyof typeof components;
// Fields only a player with a character has
const characterFields = ["character", "crafting_queue", "personal_logistic_slots"] as const;
/**
* Components synced on an instance.
*
* The controller is only synced together with the inventories, as
* switching controller can destroy the character holding them.
*/
export function enabledComponents(config: lib.InstanceConfig) {
const enabled = new Set<Component>();
for (const component of Object.keys(components) as Component[]) {
if (config.get(`player_sync.sync_${component}`)) {
enabled.add(component);
}
}
if (!enabled.has("inventories")) {
enabled.delete("controller");
}
return enabled;
}
/** Remove the fields of components which are not synced. */
export function stripPlayerData(playerData: IpcPlayerData, enabled: Set<Component>) {
const fields = playerData as Record<string, unknown>;
for (const [component, keys] of Object.entries(components)) {
if (!enabled.has(component as Component)) {
for (const key of keys) {
delete fields[key];
}
}
}
return playerData;
}
/**
* Combine data uploaded by an instance with the stored data.
*
* Components the instance does not sync keep the stored fields. Without
* the controller synced the god and ghost inventories are left as the
* synced controller had them, and character fields are only taken from a
* player which has a character.
*/
export function mergePlayerData(stored: IpcPlayerData | undefined, uploaded: IpcPlayerData, enabled: Set<Component>) {
const merged: Record<string, unknown> = { ...uploaded };
function keep(keys: readonly string[]) {
for (const key of keys) {
const value = (stored as Record<string, unknown> | undefined)?.[key];
if (value !== undefined) {
merged[key] = value;
} else {
delete merged[key];
}
}
}
for (const [component, keys] of Object.entries(components)) {
if (!enabled.has(component as Component)) {
keep(keys);
}
}
if (!enabled.has("controller")) {
keep(["inventories"]);
if (!uploaded.character) {
keep(characterFields);
}
}
return merged as IpcPlayerData;
}