mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 15:24:52 +09:00
Send only recipe notification changes on inventory download
Lua includes its current cleared recipe list in the download request. The instance plugin diffs it against the stored list and sends back the names to add and remove instead of the full list. Lua rebuilds the full list from the snapshot kept in the download record and runs the existing deserializer, so the end state is unchanged. Also stop the serializer from appending to the stored failed recipe list in place, which grew the list on every upload without a download in between. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
7987a5d925
commit
0d6beeba47
6 files changed
+210
-10
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import util from "util";
|
||||
import zlib from "zlib";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
import {
|
||||
@@ -8,6 +10,11 @@ type IpcPlayerName = {
|
||||
player_name: string
|
||||
}
|
||||
|
||||
type IpcDownloadRequest = {
|
||||
player_name: string,
|
||||
recipe_notifications?: string,
|
||||
}
|
||||
|
||||
type IpcAcquireResponse = {
|
||||
player_name: string,
|
||||
status: string,
|
||||
@@ -26,6 +33,49 @@ function chunkify(chunkSize: number, string: string): string[] {
|
||||
return string.match(new RegExp(`.{1,${chunkSize}}`, "g")) || [];
|
||||
}
|
||||
|
||||
const inflate = util.promisify(zlib.inflate);
|
||||
const deflate = util.promisify(zlib.deflate);
|
||||
|
||||
/**
|
||||
* Decode a string produced by helpers.encode_string in Factorio
|
||||
*/
|
||||
async function decodeLuaString(encoded: string): Promise<string> {
|
||||
return (await inflate(Buffer.from(encoded, "base64"))).toString("utf8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a string so that helpers.decode_string in Factorio can read it
|
||||
*/
|
||||
async function encodeLuaString(text: string): Promise<string> {
|
||||
return (await deflate(Buffer.from(text, "utf8"))).toString("base64");
|
||||
}
|
||||
|
||||
export type RecipeNotificationDelta = {
|
||||
add?: string[],
|
||||
remove?: string[],
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the changes needed to turn the current cleared recipe list into the stored one
|
||||
* @param stored - Cleared recipe names stored on the controller
|
||||
* @param current - Cleared recipe names the instance currently has
|
||||
* @returns names to add to and remove from the current list
|
||||
*/
|
||||
export function recipeNotificationDelta(stored: string[], current: string[]): RecipeNotificationDelta {
|
||||
const storedSet = new Set(stored);
|
||||
const currentSet = new Set(current);
|
||||
const delta: RecipeNotificationDelta = {};
|
||||
const add = stored.filter(name => !currentSet.has(name));
|
||||
const remove = current.filter(name => !storedSet.has(name));
|
||||
if (add.length) {
|
||||
delta.add = add;
|
||||
}
|
||||
if (remove.length) {
|
||||
delta.remove = remove;
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
export class InstancePlugin extends BaseInstancePlugin {
|
||||
playersToRelease!: Set<string>;
|
||||
disconnecting!: boolean;
|
||||
@@ -55,7 +105,7 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
);
|
||||
this.instance.server.on(
|
||||
"ipc-inventory_sync_download",
|
||||
(request: IpcPlayerName) => this.handleDownload(request).catch(
|
||||
(request: IpcDownloadRequest) => this.handleDownload(request).catch(
|
||||
err => this.logger.error(`Error handling ipc-inventory_sync_download:\n${err.stack}`)
|
||||
),
|
||||
);
|
||||
@@ -163,7 +213,27 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
);
|
||||
}
|
||||
|
||||
async handleDownload(request: IpcPlayerName) {
|
||||
/**
|
||||
* Replace the stored recipe notifications with the difference from the
|
||||
* current state on the instance to reduce the amount of data sent to Lua.
|
||||
*/
|
||||
async applyRecipeNotificationDelta(playerData: IpcPlayerData, current: string) {
|
||||
if (!playerData.recipe_notifications) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const delta = recipeNotificationDelta(
|
||||
JSON.parse(await decodeLuaString(playerData.recipe_notifications)),
|
||||
JSON.parse(await decodeLuaString(current)),
|
||||
);
|
||||
playerData.recipe_notifications_delta = await encodeLuaString(JSON.stringify(delta));
|
||||
delete playerData.recipe_notifications;
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Sending full recipe notifications for ${playerData.name}:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleDownload(request: IpcDownloadRequest) {
|
||||
const playerName = request.player_name;
|
||||
this.logger.verbose(`Downloading ${playerName}`);
|
||||
|
||||
@@ -177,6 +247,10 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.recipe_notifications) {
|
||||
await this.applyRecipeNotificationDelta(response.playerData, request.recipe_notifications);
|
||||
}
|
||||
|
||||
const chunkSize = this.instance.config.get("inventory_sync.rcon_chunk_size");
|
||||
const chunks = chunkify(chunkSize, JSON.stringify(response.playerData));
|
||||
this.logger.verbose(`Sending inventory for ${playerName} in ${chunks.length} chunks`);
|
||||
|
||||
Reference in new issue
Block a user