mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
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>
83 lines
2.8 KiB
TypeScript
83 lines
2.8 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", "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;
|
|
}
|