Files
factorio-clusterio/plugins/player_sync/components.ts
T
ILLISISandClaude Fable 5.2 b781731ecc Sync the page shown on each quick bar row
player_sync restored the content of the quick bar slots but not which
page each row shows, which every save keeps per player on its own. After
switching servers the rows could show other pages, and with them another
set of slots.

Fixes #1201

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JyNnVzRW4jPWaoWTxCyhM7
2026-11-13 22:14:46 +11:00

83 lines
2.9 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 holds the god inventory and the hidden ghost inventory
inventories: ["character", "crafting_queue", "inventories"],
logistics: ["personal_logistic_slots"],
quick_bar: ["quick_bar", "quick_bar_pages", "hotbar"],
settings: ["flashlight", "shortcuts", "game_view_settings"],
recipe_notifications: ["recipe_notifications"],
} as const;
export type Component = keyof typeof components;
function componentSet(isEnabled: (component: Component) => boolean) {
return new Set((Object.keys(components) as Component[]).filter(isEnabled));
}
/** Components synced on an instance. */
export function enabledComponents(config: lib.InstanceConfig) {
return componentSet(component => Boolean(config.get(`player_sync.sync_${component}`)));
}
/**
* 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 scenarioComponents(sent: Record<string, boolean> | unknown[] | undefined) {
if (sent === undefined) {
return componentSet(() => true);
}
return componentSet(component => !Array.isArray(sent) && sent[component] === true);
}
/** 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 upload was not serialized with keep the stored fields.
* Logistic requests are kept when the player has nowhere to hold them.
*/
export function mergePlayerData(stored: IpcPlayerData | undefined, uploaded: IpcPlayerData) {
const { components: uploadComponents, ...merged } = uploaded as IpcPlayerData & Record<string, unknown>;
const enabled = scenarioComponents(uploadComponents);
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 (!uploaded.character && !uploaded.personal_logistic_slots) {
keep(["personal_logistic_slots"]);
}
return merged as IpcPlayerData;
}