mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 07:24:52 +09:00
Rename inventory_sync to player_sync (#1072)
* Let plugins migrate the data core stores for them Plugins had no way to update the config fields and permissions core stores for them, so renaming inventory_sync needed migrations in core packages. A plugin can now declare a migrationEntrypoint. It's loaded on the controller and hosts before configs and roles are read, for every plugin in the plugin list, and attaches handlers to sync hooks for the controller, host and instance configs and roles. lib gets helpers to rename config fields and permissions, and each config gets a hidden <plugin>.version field set after load so migrations can tell which version saved the data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Write plugin versions while loading configs The staging copy made by Config.set() went through the constructor, so plugin migrations ran a second time on the live fields. A version-gated migration was applied twice when setPluginVersions() changed the version. Staging now copies the fields with update() instead. A config saved without <plugin>.version got the running version as its default, so setPluginVersions() saw no change and nothing saved the stamp. The constructor now writes the registered plugin versions after the migrations run and marks the config dirty when that changes a loaded value. Controller.bootstrap() saves the instances that came out dirty, and setPluginVersions() is gone. Restructure the SyncHook and plugin config tests by method, and suggest the controller config version for role migrations in the docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Rename inventory_sync to player_sync The plugin syncs more than inventories now (settings, appearance, recipe notifications), so name it for what it does. The package becomes @clusterio/plugin-player_sync and every inventory_sync identifier follows. A migration entrypoint renames the inventory_sync config fields, including load_plugin, and the inventory view permission. The plugin moves inventories.json in the database directory to player_sync.json and storage.inventory_sync in saves to storage.player_sync itself, and closes dialogs left open under the old GUI names on startup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Let old saves through the player left guard on_pre_player_left_game runs before on_server_startup when an old autosave with connected players is loaded, so the data is still under inventory_sync. The guard returned before get_script_data() could move it, and the recovery upload was never sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
ef943f22a4
39 files changed
+332
-177
No files matched your search
@@ -75,7 +75,7 @@ These are the plugins supported and maintained by the Clusterio developers:
|
||||
- [Statistics Exporter](/plugins/statistics_exporter/README.md): collect in-game statistics from all the servers and makes it available to the Prometheus endpoint on the controller.
|
||||
- [Subspace Storage](https://github.com/clusterio/subspace_storage): Provide shared storage that can transport items between servers via teleport chests.
|
||||
- [Player Auth](/plugins/player_auth/README.md): Provides authentication to the cluster via logging into a Factorio server.
|
||||
- [Inventory Sync](/plugins/inventory_sync/README.md): Synchronizes your inventory between servers.
|
||||
- [Player Sync](/plugins/player_sync/README.md): Synchronizes your inventory and player settings between servers.
|
||||
|
||||
There's also plugins developed and maintained by the community:
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ if (process.platform === "win32") {
|
||||
|
||||
const availablePlugins = [
|
||||
{ name: "Global Chat", value: "@clusterio/plugin-global_chat" },
|
||||
{ name: "Inventory Sync", value: "@clusterio/plugin-inventory_sync" },
|
||||
{ name: "Player Auth", value: "@clusterio/plugin-player_auth" },
|
||||
{ name: "Player Sync", value: "@clusterio/plugin-player_sync" },
|
||||
{ name: "Research Sync", value: "@clusterio/plugin-research_sync" },
|
||||
{ name: "Statistics Exporter", value: "@clusterio/plugin-statistics_exporter" },
|
||||
{ name: "Subspace Storage", value: "@clusterio/plugin-subspace_storage" },
|
||||
|
||||
@@ -119,9 +119,9 @@ By default the freeplay scenario from the latest Factorio install in `host.facto
|
||||
* `--zip` Write the scenario as a zip file instead of a directory.
|
||||
Factorio does not load zipped scenarios, extract it before use.
|
||||
|
||||
For example, creating a scenario with only the inventory_sync module:
|
||||
For example, creating a scenario with only the player_sync module:
|
||||
|
||||
npx clusteriohost create-scenario ~/.factorio/scenarios/inventory_sync --plugins inventory_sync
|
||||
npx clusteriohost create-scenario ~/.factorio/scenarios/player_sync --plugins player_sync
|
||||
|
||||
|
||||
#### `config list`
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
local compat = require("modules/clusterio/compat")
|
||||
|
||||
--- @param no_early_return boolean? True to always setup data
|
||||
return function(no_early_return)
|
||||
local inventory_sync = compat.script_data.inventory_sync
|
||||
if inventory_sync and not no_early_return then
|
||||
return inventory_sync
|
||||
end
|
||||
|
||||
inventory_sync = inventory_sync or {}
|
||||
inventory_sync.players = inventory_sync.players or {}
|
||||
inventory_sync.players_waiting_for_acquire = inventory_sync.players_waiting_for_acquire or {}
|
||||
inventory_sync.players_in_cutscene_to_sync = inventory_sync.players_in_cutscene_to_sync or {}
|
||||
inventory_sync.failed_deserialization = inventory_sync.failed_deserialization or {}
|
||||
|
||||
inventory_sync.active_downloads = inventory_sync.active_downloads or {}
|
||||
inventory_sync.finished_downloads = inventory_sync.finished_downloads or {}
|
||||
inventory_sync.active_uploads = inventory_sync.active_uploads or {}
|
||||
|
||||
compat.script_data.inventory_sync = inventory_sync
|
||||
return inventory_sync
|
||||
end
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"name": "inventory_sync",
|
||||
"load": ["inventory_sync.lua", "gui/dialog_failed_download.lua"]
|
||||
}
|
||||
File renamed without changes.
@@ -1,6 +1,6 @@
|
||||
# Clusterio inventory sync plugin
|
||||
# Clusterio player sync plugin
|
||||
|
||||
Carry over player inventory between servers
|
||||
Carry over player inventories and settings between servers
|
||||
|
||||
As a player, it mostly just works. Most important to know is that factorio data transfer is slow, which means if you have "big" items (blueprints mostly) in your inventory it will take a long time to transfer when joining servers. For this reason it is recommended to keep blueprints in the blueprint library.
|
||||
|
||||
@@ -12,11 +12,24 @@ When a server crashes while you are online there won't be time to immediately up
|
||||
|
||||
Run the following commands in the folder Clusterio is installed to:
|
||||
|
||||
npm install @clusterio/plugin-inventory_sync
|
||||
npx clusteriocontroller plugin add @clusterio/plugin-inventory_sync
|
||||
npm install @clusterio/plugin-player_sync
|
||||
npx clusteriocontroller plugin add @clusterio/plugin-player_sync
|
||||
|
||||
Substitute clusteriocontroller with clusteriohost or clusterioctl if this a dedicated host or ctl installation respectively.
|
||||
|
||||
## Upgrading from inventory_sync
|
||||
|
||||
This plugin was called inventory_sync before 2.0.0-alpha.28.
|
||||
Remove the old package and install the new one in its place:
|
||||
|
||||
npm uninstall @clusterio/plugin-inventory_sync
|
||||
npm install @clusterio/plugin-player_sync
|
||||
npx clusteriocontroller plugin remove inventory_sync
|
||||
npx clusteriocontroller plugin add @clusterio/plugin-player_sync
|
||||
|
||||
The `inventory_sync.*` config fields and the `inventory_sync.inventory.view` permission are renamed on load.
|
||||
The controller moves `inventories.json` in the database directory to `player_sync.json`, and saves keep their player data the next time they are started with the new module.
|
||||
|
||||
## Method of operation
|
||||
|
||||
This plugin does event based synchronization of inventories.
|
||||
@@ -47,7 +60,7 @@ Overall, we are able to achieve a latency between 3 and infinite ticks from serv
|
||||
|
||||
## Controller Configuration
|
||||
|
||||
### inventory_sync.player_lock_timeout
|
||||
### player_sync.player_lock_timeout
|
||||
|
||||
Time in seconds before the exclusive access an instance holds on a player's inventory expires after the instance stops or its host disconnects.
|
||||
Until it expires the player is told the inventory is in use on that instance when joining another one.
|
||||
@@ -58,7 +71,7 @@ Defaults to `60`.
|
||||
|
||||
## Instance Configuration
|
||||
|
||||
### inventory_sync.rcon_chunk_size
|
||||
### player_sync.rcon_chunk_size
|
||||
|
||||
Size in characters of the chunks the serialized inventory is split into when sent to the game over RCON.
|
||||
Smaller chunks take more commands to transfer an inventory but hold up the RCON command pipe for less time each.
|
||||
@@ -7,7 +7,20 @@ import * as lib from "@clusterio/lib";
|
||||
import * as msg from "./messages.js";
|
||||
|
||||
async function loadDatabase(config: lib.ControllerConfig, logger: lib.Logger) {
|
||||
let itemsPath = path.resolve(config.get("controller.database_directory"), "inventories.json");
|
||||
let databaseDirectory = config.get("controller.database_directory");
|
||||
let itemsPath = path.resolve(databaseDirectory, "player_sync.json");
|
||||
// migrate: inventory_sync stored its data in inventories.json before alpha 28
|
||||
let legacyPath = path.resolve(databaseDirectory, "inventories.json");
|
||||
if (!await fs.access(itemsPath).then(() => true, () => false)) {
|
||||
try {
|
||||
await fs.rename(legacyPath, itemsPath);
|
||||
logger.info(`Moved ${legacyPath} to ${itemsPath}`);
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.verbose(`Loading ${itemsPath}`);
|
||||
try {
|
||||
let content = await fs.readFile(itemsPath, { encoding: "utf8" });
|
||||
@@ -28,7 +41,7 @@ async function saveDatabase(
|
||||
logger: lib.Logger,
|
||||
) {
|
||||
if (playerDatastore) {
|
||||
let file = path.resolve(controllerConfig.get("controller.database_directory"), "inventories.json");
|
||||
let file = path.resolve(controllerConfig.get("controller.database_directory"), "player_sync.json");
|
||||
logger.verbose(`writing ${file}`);
|
||||
let content = JSON.stringify(Array.from(playerDatastore));
|
||||
await lib.safeOutputFile(file, content);
|
||||
@@ -67,7 +80,7 @@ export default async function(context: ControllerPluginContext) {
|
||||
}
|
||||
|
||||
if (["unknown", "stopped"].includes(instance.status)) {
|
||||
let timeoutMs = controller.config.get("inventory_sync.player_lock_timeout") * 1000;
|
||||
let timeoutMs = controller.config.get("player_sync.player_lock_timeout") * 1000;
|
||||
for (let acquisitonRecord of acquiredPlayers.values()) {
|
||||
if (acquisitonRecord.instanceId === instanceId && !acquisitonRecord.expiresMs) {
|
||||
acquisitonRecord.expiresMs = Date.now() + timeoutMs;
|
||||
@@ -3,24 +3,25 @@ import * as messages from "./messages.js";
|
||||
|
||||
declare module "@clusterio/lib" {
|
||||
export interface InstanceConfigFields {
|
||||
"inventory_sync.rcon_chunk_size": number;
|
||||
"player_sync.rcon_chunk_size": number;
|
||||
}
|
||||
export interface ControllerConfigFields {
|
||||
"inventory_sync.player_lock_timeout": number;
|
||||
"player_sync.player_lock_timeout": number;
|
||||
}
|
||||
export interface Permissions {
|
||||
"inventory_sync.inventory.view": never;
|
||||
"player_sync.inventory.view": never;
|
||||
}
|
||||
}
|
||||
|
||||
export const plugin: lib.PluginDeclaration = {
|
||||
name: "inventory_sync",
|
||||
title: "Inventory sync",
|
||||
description: "Synchronizes players inventories between instances",
|
||||
name: "player_sync",
|
||||
title: "Player sync",
|
||||
description: "Synchronizes player inventories and settings between instances",
|
||||
migrationEntrypoint: "dist/node/migrations.js",
|
||||
|
||||
instanceEntrypoint: "dist/node/instance.js",
|
||||
instanceConfigFields: {
|
||||
"inventory_sync.rcon_chunk_size": {
|
||||
"player_sync.rcon_chunk_size": {
|
||||
title: "Rcon inventory chunk size",
|
||||
description:
|
||||
"Divide inventories into chunks of this size before sending with rcon to prevent blocking the pipe",
|
||||
@@ -31,7 +32,7 @@ export const plugin: lib.PluginDeclaration = {
|
||||
|
||||
controllerEntrypoint: "dist/node/controller.js",
|
||||
controllerConfigFields: {
|
||||
"inventory_sync.player_lock_timeout": {
|
||||
"player_sync.player_lock_timeout": {
|
||||
title: "Player Lock Timeout",
|
||||
description:
|
||||
"Time in seconds before the lock on a player inventory expires after an instance stops " +
|
||||
@@ -55,7 +56,7 @@ export const plugin: lib.PluginDeclaration = {
|
||||
],
|
||||
permissions: [
|
||||
{
|
||||
name: "inventory_sync.inventory.view",
|
||||
name: "player_sync.inventory.view",
|
||||
title: "View player inventories",
|
||||
description: "View player inventories",
|
||||
grantByDefault: true,
|
||||
@@ -136,7 +136,7 @@ export default async function(context: InstancePluginContext) {
|
||||
}
|
||||
|
||||
let json = lib.escapeString(JSON.stringify(response));
|
||||
await instance.sendRcon(`/sc inventory_sync.acquire_response("${json}")`, true, plugin.name);
|
||||
await instance.sendRcon(`/sc player_sync.acquire_response("${json}")`, true, plugin.name);
|
||||
}
|
||||
|
||||
async function handleRelease(request: IpcPlayerName) {
|
||||
@@ -178,7 +178,7 @@ export default async function(context: InstancePluginContext) {
|
||||
}
|
||||
|
||||
await instance.sendRcon(
|
||||
`/sc inventory_sync.confirm_upload("${player_data.name}", ${player_data.generation})`, true, plugin.name
|
||||
`/sc player_sync.confirm_upload("${player_data.name}", ${player_data.generation})`, true, plugin.name
|
||||
);
|
||||
}
|
||||
|
||||
@@ -193,20 +193,20 @@ export default async function(context: InstancePluginContext) {
|
||||
|
||||
if (!response.playerData) {
|
||||
await instance.sendRcon(
|
||||
`/sc inventory_sync.download_inventory('${playerName}',nil,0,0)`, true, plugin.name
|
||||
`/sc player_sync.download_inventory('${playerName}',nil,0,0)`, true, plugin.name
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await applyRecipeNotificationDelta(response.playerData, request.recipe_notifications, logger);
|
||||
|
||||
const chunkSize = instance.config.get("inventory_sync.rcon_chunk_size");
|
||||
const chunkSize = instance.config.get("player_sync.rcon_chunk_size");
|
||||
const chunks = chunkify(chunkSize, JSON.stringify(response.playerData));
|
||||
logger.verbose(`Sending inventory for ${playerName} in ${chunks.length} chunks`);
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunk = lib.escapeString(chunks[i]);
|
||||
await instance.sendRcon(
|
||||
`/sc inventory_sync.download_inventory('${playerName}','${chunk}',${i + 1},${chunks.length})`,
|
||||
`/sc player_sync.download_inventory('${playerName}','${chunk}',${i + 1},${chunks.length})`,
|
||||
true,
|
||||
plugin.name,
|
||||
);
|
||||
@@ -215,27 +215,27 @@ export default async function(context: InstancePluginContext) {
|
||||
|
||||
// Handle IPC from scenario script
|
||||
instance.server.on(
|
||||
"ipc-inventory_sync_acquire",
|
||||
"ipc-player_sync_acquire",
|
||||
(request: IpcPlayerName) => handleAcquire(request).catch(
|
||||
err => logger.error(`Error handling ipc-inventory_sync_acquire:\n${err.stack}`)
|
||||
err => logger.error(`Error handling ipc-player_sync_acquire:\n${err.stack}`)
|
||||
),
|
||||
);
|
||||
instance.server.on(
|
||||
"ipc-inventory_sync_release",
|
||||
"ipc-player_sync_release",
|
||||
(request: IpcPlayerName) => handleRelease(request).catch(
|
||||
err => logger.error(`Error handling ipc-inventory_sync_release:\n${err.stack}`)
|
||||
err => logger.error(`Error handling ipc-player_sync_release:\n${err.stack}`)
|
||||
),
|
||||
);
|
||||
instance.server.on(
|
||||
"ipc-inventory_sync_upload",
|
||||
"ipc-player_sync_upload",
|
||||
(player_data: IpcPlayerData) => handleUpload(player_data).catch(
|
||||
err => logger.error(`Error handling ipc-inventory_sync_upload:\n${err.stack}`)
|
||||
err => logger.error(`Error handling ipc-player_sync_upload:\n${err.stack}`)
|
||||
),
|
||||
);
|
||||
instance.server.on(
|
||||
"ipc-inventory_sync_download",
|
||||
"ipc-player_sync_download",
|
||||
(request: IpcDownloadRequest) => handleDownload(request).catch(
|
||||
err => logger.error(`Error handling ipc-inventory_sync_download:\n${err.stack}`)
|
||||
err => logger.error(`Error handling ipc-player_sync_download:\n${err.stack}`)
|
||||
),
|
||||
);
|
||||
|
||||
@@ -53,7 +53,7 @@ export class AcquireRequest {
|
||||
static type = "request" as const;
|
||||
static src = "instance" as const;
|
||||
static dst = "controller" as const;
|
||||
static plugin = "inventory_sync" as const;
|
||||
static plugin = "player_sync" as const;
|
||||
static Response = AcquireResponse;
|
||||
|
||||
constructor(
|
||||
@@ -77,7 +77,7 @@ export class ReleaseRequest {
|
||||
static type = "request" as const;
|
||||
static src = "instance" as const;
|
||||
static dst = "controller" as const;
|
||||
static plugin = "inventory_sync" as const;
|
||||
static plugin = "player_sync" as const;
|
||||
|
||||
constructor(
|
||||
public instanceId: number,
|
||||
@@ -100,7 +100,7 @@ export class UploadRequest {
|
||||
static type = "request" as const;
|
||||
static src = "instance" as const;
|
||||
static dst = "controller" as const;
|
||||
static plugin = "inventory_sync" as const;
|
||||
static plugin = "player_sync" as const;
|
||||
|
||||
constructor(
|
||||
public instanceId: number,
|
||||
@@ -140,7 +140,7 @@ export class DownloadRequest {
|
||||
static type = "request" as const;
|
||||
static src = "instance" as const;
|
||||
static dst = "controller" as const;
|
||||
static plugin = "inventory_sync" as const;
|
||||
static plugin = "player_sync" as const;
|
||||
static Response = DownloadResponse;
|
||||
|
||||
constructor(
|
||||
@@ -186,7 +186,7 @@ export class DatabaseStatsRequest {
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static dst = "controller" as const;
|
||||
static plugin = "inventory_sync" as const;
|
||||
static permission = "inventory_sync.inventory.view" as const;
|
||||
static plugin = "player_sync" as const;
|
||||
static permission = "player_sync.inventory.view" as const;
|
||||
static Response = DatabaseStatsResponse;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
export default function(context: lib.MigrationContext) {
|
||||
const { hooks, plugin } = context;
|
||||
// migrate: inventory_sync was renamed to player_sync in 2.0.0-alpha.28
|
||||
const renameFields = (config: lib.ConfigSchema) => (
|
||||
lib.renameConfigPrefix(config, "inventory_sync.", "player_sync.")
|
||||
);
|
||||
hooks.controllerConfig.attach(plugin.name, renameFields);
|
||||
hooks.hostConfig.attach(plugin.name, renameFields);
|
||||
hooks.instanceConfig.attach(plugin.name, renameFields);
|
||||
hooks.role.attach(plugin.name, role => lib.renamePermissionPrefix(role, "inventory_sync.", "player_sync."));
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+8
-8
@@ -1,7 +1,7 @@
|
||||
local progress_dialog = require("modules/inventory_sync/gui/progress_dialog")
|
||||
local ensure_character = require("modules/inventory_sync/ensure_character")
|
||||
local restore_position = require("modules/inventory_sync/restore_position")
|
||||
local get_script_data = require("modules/inventory_sync/get_script_data")
|
||||
local progress_dialog = require("modules/player_sync/gui/progress_dialog")
|
||||
local ensure_character = require("modules/player_sync/ensure_character")
|
||||
local restore_position = require("modules/player_sync/restore_position")
|
||||
local get_script_data = require("modules/player_sync/get_script_data")
|
||||
|
||||
|
||||
local function download_inventory(player_name, data, number, total)
|
||||
@@ -21,14 +21,14 @@ local function download_inventory(player_name, data, number, total)
|
||||
local player_record = script_data.players[player_name]
|
||||
if record.restart then
|
||||
rcon.print("Restarting outdated download")
|
||||
inventory_sync.initiate_inventory_download(player, player_record, record.generation)
|
||||
player_sync.initiate_inventory_download(player, player_record, record.generation)
|
||||
return
|
||||
end
|
||||
|
||||
if total == 0 then
|
||||
-- Download was requested but the player had no data stored.
|
||||
log("ERROR: Inventory sync failed, got empty player data for " .. player.name .. " from the controller")
|
||||
player.print("ERROR: Inventory sync failed, got empty player data from the controller")
|
||||
log("ERROR: Player sync failed, got empty player data for " .. player.name .. " from the controller")
|
||||
player.print("ERROR: Player sync failed, got empty player data from the controller")
|
||||
|
||||
progress_dialog.remove(player)
|
||||
|
||||
@@ -70,7 +70,7 @@ local function download_inventory(player_name, data, number, total)
|
||||
log("Imported player data for " .. player_name .. " in " .. ticks .. " ticks")
|
||||
|
||||
if player.connected then
|
||||
inventory_sync.finish_download(player, record)
|
||||
player_sync.finish_download(player, record)
|
||||
end
|
||||
end
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,28 @@
|
||||
local compat = require("modules/clusterio/compat")
|
||||
|
||||
--- @param no_early_return boolean? True to always setup data
|
||||
return function(no_early_return)
|
||||
local player_sync = compat.script_data.player_sync
|
||||
if player_sync and not no_early_return then
|
||||
return player_sync
|
||||
end
|
||||
|
||||
-- migrate: inventory_sync was renamed to player_sync in 2.0.0-alpha.28
|
||||
if not player_sync and compat.script_data.inventory_sync then
|
||||
player_sync = compat.script_data.inventory_sync
|
||||
compat.script_data.inventory_sync = nil
|
||||
end
|
||||
|
||||
player_sync = player_sync or {}
|
||||
player_sync.players = player_sync.players or {}
|
||||
player_sync.players_waiting_for_acquire = player_sync.players_waiting_for_acquire or {}
|
||||
player_sync.players_in_cutscene_to_sync = player_sync.players_in_cutscene_to_sync or {}
|
||||
player_sync.failed_deserialization = player_sync.failed_deserialization or {}
|
||||
|
||||
player_sync.active_downloads = player_sync.active_downloads or {}
|
||||
player_sync.finished_downloads = player_sync.finished_downloads or {}
|
||||
player_sync.active_uploads = player_sync.active_uploads or {}
|
||||
|
||||
compat.script_data.player_sync = player_sync
|
||||
return player_sync
|
||||
end
|
||||
+12
-12
@@ -1,5 +1,5 @@
|
||||
local ensure_character = require("modules/inventory_sync/ensure_character")
|
||||
local get_script_data = require("modules/inventory_sync/get_script_data")
|
||||
local ensure_character = require("modules/player_sync/ensure_character")
|
||||
local get_script_data = require("modules/player_sync/get_script_data")
|
||||
|
||||
local dialog_failed_download = {}
|
||||
|
||||
@@ -21,7 +21,7 @@ function dialog_failed_download.create(player, acquire_response)
|
||||
}
|
||||
titlebar.drag_target = frame
|
||||
|
||||
local title_caption = "Inventory sync download failed"
|
||||
local title_caption = "Player sync download failed"
|
||||
if acquire_response.status == "busy" then
|
||||
title_caption = "Inventory is in use on " .. acquire_response.message
|
||||
end
|
||||
@@ -43,7 +43,7 @@ function dialog_failed_download.create(player, acquire_response)
|
||||
filler.drag_target = frame
|
||||
|
||||
local content = frame.add {
|
||||
name = "inventory_sync_failed_download_content",
|
||||
name = "player_sync_failed_download_content",
|
||||
type = "frame",
|
||||
direction = "vertical",
|
||||
style = "inside_shallow_frame_with_padding",
|
||||
@@ -90,7 +90,7 @@ function dialog_failed_download.create(player, acquire_response)
|
||||
dialog_row.drag_target = frame
|
||||
|
||||
local abort = dialog_row.add {
|
||||
name = "inventory_sync_failed_download_abort",
|
||||
name = "player_sync_failed_download_abort",
|
||||
type = "button",
|
||||
style = "back_button",
|
||||
caption = "Use temporary inventory",
|
||||
@@ -107,7 +107,7 @@ function dialog_failed_download.create(player, acquire_response)
|
||||
dialog_filler.drag_target = frame
|
||||
|
||||
local retry = dialog_row.add {
|
||||
name = "inventory_sync_failed_download_retry",
|
||||
name = "player_sync_failed_download_retry",
|
||||
type = "button",
|
||||
style = "forward_button",
|
||||
caption = "Retry",
|
||||
@@ -123,19 +123,19 @@ dialog_failed_download.events = {
|
||||
return
|
||||
end
|
||||
|
||||
if event.element.name == "inventory_sync_failed_download_abort" then
|
||||
if event.element.name == "player_sync_failed_download_abort" then
|
||||
-- Give the player a character and let them play with that
|
||||
local player = assert(game.get_player(event.player_index))
|
||||
ensure_character(player)
|
||||
local inventory_sync = get_script_data()
|
||||
local player_sync = get_script_data()
|
||||
player.gui.screen.dialog_failed_download.destroy()
|
||||
inventory_sync.players[player.name].dirty = true
|
||||
inventory_sync.players[player.name].sync = false
|
||||
player_sync.players[player.name].dirty = true
|
||||
player_sync.players[player.name].sync = false
|
||||
|
||||
elseif event.element.name == "inventory_sync_failed_download_retry" then
|
||||
elseif event.element.name == "player_sync_failed_download_retry" then
|
||||
-- Retry acquiring the player from the controller
|
||||
local player = assert(game.get_player(event.player_index))
|
||||
inventory_sync.acquire(player)
|
||||
player_sync.acquire(player)
|
||||
player.gui.screen.dialog_failed_download.destroy()
|
||||
end
|
||||
end,
|
||||
+7
-7
@@ -6,7 +6,7 @@ local progress_dialog = {}
|
||||
--- @param total number
|
||||
function progress_dialog.create(player, progress, total)
|
||||
local frame = player.gui.screen.add {
|
||||
name = "inventory_sync_progress",
|
||||
name = "player_sync_progress",
|
||||
type = "frame",
|
||||
direction = "vertical",
|
||||
}
|
||||
@@ -19,7 +19,7 @@ function progress_dialog.create(player, progress, total)
|
||||
local title = titlebar.add {
|
||||
type = "label",
|
||||
style = "frame_title",
|
||||
caption = "Inventory sync",
|
||||
caption = "Player sync",
|
||||
}
|
||||
title.drag_target = frame
|
||||
|
||||
@@ -33,7 +33,7 @@ function progress_dialog.create(player, progress, total)
|
||||
filler.drag_target = frame
|
||||
|
||||
local content = frame.add {
|
||||
name = "inventory_sync_progress_dialog_content",
|
||||
name = "player_sync_progress_dialog_content",
|
||||
type = "frame",
|
||||
direction = "vertical",
|
||||
style = "inside_shallow_frame_with_padding",
|
||||
@@ -59,8 +59,8 @@ end
|
||||
|
||||
--- @param player LuaPlayer
|
||||
function progress_dialog.remove(player)
|
||||
if player.gui.screen.inventory_sync_progress then
|
||||
player.gui.screen.inventory_sync_progress.destroy()
|
||||
if player.gui.screen.player_sync_progress then
|
||||
player.gui.screen.player_sync_progress.destroy()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -69,12 +69,12 @@ end
|
||||
--- @param total number
|
||||
function progress_dialog.update(player, progress, total)
|
||||
-- Be defensive here in case an old GUI is present
|
||||
local frame = player.gui.screen.inventory_sync_progress
|
||||
local frame = player.gui.screen.player_sync_progress
|
||||
if not frame then
|
||||
return false
|
||||
end
|
||||
|
||||
local content = frame.inventory_sync_progress_dialog_content
|
||||
local content = frame.player_sync_progress_dialog_content
|
||||
if not content then
|
||||
return false
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "player_sync",
|
||||
"load": ["player_sync.lua", "gui/dialog_failed_download.lua"]
|
||||
}
|
||||
+69
-59
@@ -1,6 +1,6 @@
|
||||
--[[
|
||||
|
||||
Inventory Sync
|
||||
Player Sync
|
||||
|
||||
When a player leaves the game, serialize the player state and upload it to the controller.
|
||||
When a player joins the game, request the player data from controller and deserialize the player state from it.
|
||||
@@ -9,12 +9,12 @@ When a player joins the game, request the player data from controller and deseri
|
||||
|
||||
local compat = require("modules/clusterio/compat")
|
||||
local clusterio_api = require("modules/clusterio/api")
|
||||
local download_inventory = require("modules/inventory_sync/download_inventory")
|
||||
local serialize = require("modules/inventory_sync/serialize")
|
||||
local restore_position = require("modules/inventory_sync/restore_position")
|
||||
local get_script_data = require("modules/inventory_sync/get_script_data")
|
||||
local progress_dialog = require("modules/inventory_sync/gui/progress_dialog")
|
||||
local dialog_failed_download = require("modules/inventory_sync/gui/dialog_failed_download")
|
||||
local download_inventory = require("modules/player_sync/download_inventory")
|
||||
local serialize = require("modules/player_sync/serialize")
|
||||
local restore_position = require("modules/player_sync/restore_position")
|
||||
local get_script_data = require("modules/player_sync/get_script_data")
|
||||
local progress_dialog = require("modules/player_sync/gui/progress_dialog")
|
||||
local dialog_failed_download = require("modules/player_sync/gui/dialog_failed_download")
|
||||
|
||||
local v2_remote_controller = compat.version_ge("2.0.0")
|
||||
local recipe_notifications_api = compat.version_ge("2.0.67")
|
||||
@@ -47,13 +47,13 @@ local function remove_player(player)
|
||||
end
|
||||
|
||||
|
||||
inventory_sync = {}
|
||||
-- This function is called by instance.js as /sc inventory_sync.download_inventory("danielv", Escaped JSON string, package_number, total_packages_count) with data from the controller
|
||||
inventory_sync.download_inventory = download_inventory
|
||||
player_sync = {}
|
||||
-- This function is called by instance.js as /sc player_sync.download_inventory("danielv", Escaped JSON string, package_number, total_packages_count) with data from the controller
|
||||
player_sync.download_inventory = download_inventory
|
||||
|
||||
-- This function is called internally when a player leaves the game to
|
||||
-- serialize the player for upload.
|
||||
function inventory_sync.serialize_player(player, player_record)
|
||||
function player_sync.serialize_player(player, player_record)
|
||||
-- Editor mode is not supported.
|
||||
if player.controller_type == defines.controllers.editor then
|
||||
player.toggle_map_editor()
|
||||
@@ -84,7 +84,7 @@ function inventory_sync.serialize_player(player, player_record)
|
||||
return serialized_player
|
||||
end
|
||||
|
||||
function inventory_sync.deserialize_player(player, finished_record)
|
||||
function player_sync.deserialize_player(player, finished_record)
|
||||
-- Editor mode is not supported.
|
||||
if player.controller_type == defines.controllers.editor then
|
||||
player.toggle_map_editor()
|
||||
@@ -146,8 +146,8 @@ function inventory_sync.deserialize_player(player, finished_record)
|
||||
end
|
||||
end
|
||||
|
||||
inventory_sync.events = {}
|
||||
inventory_sync.events[clusterio_api.events.on_server_startup] = function(event)
|
||||
player_sync.events = {}
|
||||
player_sync.events[clusterio_api.events.on_server_startup] = function(event)
|
||||
local script_data = get_script_data(true)
|
||||
script_data.players_waiting_for_acquire = {}
|
||||
script_data.players_in_cutscene_to_sync = {}
|
||||
@@ -156,34 +156,43 @@ inventory_sync.events[clusterio_api.events.on_server_startup] = function(event)
|
||||
if not script_data.players[player.name] then
|
||||
create_player(player, true)
|
||||
end
|
||||
|
||||
-- migrate: dialogs created before inventory_sync was renamed to player_sync
|
||||
local screen = player.gui.screen
|
||||
if screen.inventory_sync_progress then
|
||||
screen.inventory_sync_progress.destroy()
|
||||
end
|
||||
if screen.dialog_failed_download and screen.dialog_failed_download.inventory_sync_failed_download_content then
|
||||
screen.dialog_failed_download.destroy()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Cleanup
|
||||
inventory_sync.events[defines.events.on_pre_player_removed] = function(event)
|
||||
player_sync.events[defines.events.on_pre_player_removed] = function(event)
|
||||
remove_player(game.get_player(event.player_index))
|
||||
end
|
||||
|
||||
inventory_sync.events[defines.events.on_player_created] = function(event)
|
||||
player_sync.events[defines.events.on_player_created] = function(event)
|
||||
create_player(game.get_player(event.player_index), false)
|
||||
end
|
||||
|
||||
function inventory_sync.acquire(player)
|
||||
function player_sync.acquire(player)
|
||||
local script_data = get_script_data()
|
||||
script_data.players_waiting_for_acquire[player.name] = {
|
||||
start_tick = game.ticks_played,
|
||||
}
|
||||
clusterio_api.send_json("inventory_sync_acquire", {
|
||||
clusterio_api.send_json("player_sync_acquire", {
|
||||
player_name = player.name,
|
||||
})
|
||||
end
|
||||
|
||||
function inventory_sync.check_players_waiting_for_acquire()
|
||||
function player_sync.check_players_waiting_for_acquire()
|
||||
local script_data = get_script_data()
|
||||
for player_name, record in pairs(script_data.players_waiting_for_acquire) do
|
||||
-- Check if the acquire player request timed out
|
||||
if game.ticks_played > record.start_tick + 600 then
|
||||
inventory_sync.acquire_response(compat.table_to_json({
|
||||
player_sync.acquire_response(compat.table_to_json({
|
||||
status = "timeout",
|
||||
player_name = player_name,
|
||||
}))
|
||||
@@ -191,11 +200,11 @@ function inventory_sync.check_players_waiting_for_acquire()
|
||||
end
|
||||
end
|
||||
|
||||
function inventory_sync.acquire_response(data)
|
||||
function player_sync.acquire_response(data)
|
||||
local script_data = get_script_data()
|
||||
local response = assert(compat.json_to_table(data))
|
||||
if not script_data.players_waiting_for_acquire[response.player_name] then
|
||||
clusterio_api.send_json("inventory_sync_release", { player_name = response.player_name })
|
||||
clusterio_api.send_json("player_sync_release", { player_name = response.player_name })
|
||||
return
|
||||
end
|
||||
script_data.players_waiting_for_acquire[response.player_name] = nil
|
||||
@@ -205,21 +214,21 @@ function inventory_sync.acquire_response(data)
|
||||
response.player = player
|
||||
script_data.players_in_cutscene_to_sync[response.player_name] = response
|
||||
else
|
||||
inventory_sync.sync_player(response)
|
||||
player_sync.sync_player(response)
|
||||
end
|
||||
end
|
||||
|
||||
function inventory_sync.check_players_in_cutscene()
|
||||
function player_sync.check_players_in_cutscene()
|
||||
local script_data = get_script_data()
|
||||
for player_name, response in pairs(script_data.players_in_cutscene_to_sync) do
|
||||
if not is_in_cutscene(response.player) then
|
||||
script_data.players_in_cutscene_to_sync[player_name] = nil
|
||||
inventory_sync.sync_player(response)
|
||||
player_sync.sync_player(response)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function inventory_sync.sync_player(acquire_response)
|
||||
function player_sync.sync_player(acquire_response)
|
||||
local player = assert(game.get_player(acquire_response.player_name))
|
||||
|
||||
-- Editor mode is not supported.
|
||||
@@ -228,8 +237,8 @@ function inventory_sync.sync_player(acquire_response)
|
||||
end
|
||||
|
||||
if player.controller_type == defines.controllers.editor then
|
||||
log("ERROR: Inventory sync failed, unable to switch " .. player.name .. " out of editor mode")
|
||||
player.print("ERROR: Inventory sync failed, unable to switch out of editor mode")
|
||||
log("ERROR: Player sync failed, unable to switch " .. player.name .. " out of editor mode")
|
||||
player.print("ERROR: Player sync failed, unable to switch out of editor mode")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -237,7 +246,7 @@ function inventory_sync.sync_player(acquire_response)
|
||||
local finished_record = script_data.finished_downloads[acquire_response.player_name]
|
||||
if finished_record then
|
||||
if acquire_response.status == "acquired" and finished_record.generation == acquire_response.generation then
|
||||
inventory_sync.finish_download(player, finished_record)
|
||||
player_sync.finish_download(player, finished_record)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -298,7 +307,7 @@ function inventory_sync.sync_player(acquire_response)
|
||||
acquire_response.has_data
|
||||
and (not player_record.sync or acquire_response.generation > player_record.generation)
|
||||
then
|
||||
inventory_sync.initiate_inventory_download(player, player_record, acquire_response.generation)
|
||||
player_sync.initiate_inventory_download(player, player_record, acquire_response.generation)
|
||||
|
||||
else
|
||||
if acquire_response.generation == 0 then
|
||||
@@ -314,12 +323,12 @@ function inventory_sync.sync_player(acquire_response)
|
||||
end
|
||||
end
|
||||
|
||||
function inventory_sync.finish_download(player, finished_record)
|
||||
function player_sync.finish_download(player, finished_record)
|
||||
local script_data = get_script_data()
|
||||
script_data.finished_downloads[player.name] = nil
|
||||
local player_record = script_data.players[player.name]
|
||||
|
||||
local status, result = xpcall(inventory_sync.deserialize_player, debug.traceback, player, finished_record)
|
||||
local status, result = xpcall(player_sync.deserialize_player, debug.traceback, player, finished_record)
|
||||
if not status then
|
||||
log("ERROR: Deserializing player " .. player.name .. " failed: " .. result)
|
||||
player.print("ERROR: Deserializing player data failed: " .. result)
|
||||
@@ -335,43 +344,44 @@ function inventory_sync.finish_download(player, finished_record)
|
||||
end
|
||||
|
||||
-- Download inventory from controller
|
||||
inventory_sync.events[defines.events.on_player_joined_game] = function(event)
|
||||
player_sync.events[defines.events.on_player_joined_game] = function(event)
|
||||
local player = assert(game.get_player(event.player_index))
|
||||
local script_data = get_script_data()
|
||||
|
||||
if not game.is_multiplayer() then
|
||||
return player.print("Inventory sync deactivated: game is single player")
|
||||
return player.print("Player sync deactivated: game is single player")
|
||||
end
|
||||
|
||||
-- Send acquire request even if an active download is currently in progress
|
||||
inventory_sync.acquire(player)
|
||||
player_sync.acquire(player)
|
||||
|
||||
-- Clear active upload if it exists
|
||||
script_data.active_uploads[player.name] = nil
|
||||
end
|
||||
|
||||
inventory_sync.on_nth_tick = {}
|
||||
inventory_sync.on_nth_tick[33] = function(event)
|
||||
player_sync.on_nth_tick = {}
|
||||
player_sync.on_nth_tick[33] = function(event)
|
||||
-- Periodically check players currently in the process of syncing
|
||||
inventory_sync.check_players_waiting_for_acquire()
|
||||
inventory_sync.check_players_in_cutscene()
|
||||
inventory_sync.check_active_downloads()
|
||||
inventory_sync.check_active_uploads()
|
||||
player_sync.check_players_waiting_for_acquire()
|
||||
player_sync.check_players_in_cutscene()
|
||||
player_sync.check_active_downloads()
|
||||
player_sync.check_active_uploads()
|
||||
end
|
||||
|
||||
inventory_sync.events[defines.events.on_cutscene_waypoint_reached] = function(event)
|
||||
inventory_sync.check_players_in_cutscene()
|
||||
player_sync.events[defines.events.on_cutscene_waypoint_reached] = function(event)
|
||||
player_sync.check_players_in_cutscene()
|
||||
end
|
||||
|
||||
inventory_sync.events[defines.events.on_cutscene_cancelled] = function(event)
|
||||
inventory_sync.check_players_in_cutscene()
|
||||
player_sync.events[defines.events.on_cutscene_cancelled] = function(event)
|
||||
player_sync.check_players_in_cutscene()
|
||||
end
|
||||
|
||||
-- Upload inventory when a player leaves the game. Triggers on restart after crash if player was online during crash.
|
||||
inventory_sync.events[defines.events.on_pre_player_left_game] = function(event)
|
||||
player_sync.events[defines.events.on_pre_player_left_game] = function(event)
|
||||
-- for some reason, on_pre_player_left_game gets called before on_server_startup so script_data isn't ready yet
|
||||
if not compat.script_data.inventory_sync then
|
||||
log("ERROR: script data for inventory sync is not defined")
|
||||
-- migrate: saves from before the rename still have their data under inventory_sync
|
||||
if not compat.script_data.player_sync and not compat.script_data.inventory_sync then
|
||||
log("ERROR: script data for player sync is not defined")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -381,18 +391,18 @@ inventory_sync.events[defines.events.on_pre_player_left_game] = function(event)
|
||||
|
||||
-- If player has an active download release our acquisition but let the download continue
|
||||
if script_data.active_downloads[player.name] then
|
||||
clusterio_api.send_json("inventory_sync_release", { player_name = player.name })
|
||||
clusterio_api.send_json("player_sync_release", { player_name = player.name })
|
||||
return
|
||||
end
|
||||
|
||||
if not player_record.dirty or not player_record.sync then
|
||||
-- Nothing to upload, release the acquisition so other instances are not blocked
|
||||
clusterio_api.send_json("inventory_sync_release", { player_name = player.name })
|
||||
clusterio_api.send_json("player_sync_release", { player_name = player.name })
|
||||
return
|
||||
end
|
||||
|
||||
player_record.generation = player_record.generation + 1
|
||||
local status, result = xpcall(inventory_sync.serialize_player, debug.traceback, player, player_record)
|
||||
local status, result = xpcall(player_sync.serialize_player, debug.traceback, player, player_record)
|
||||
if not status then
|
||||
log("ERROR: Serializing player " .. player.name .. " failed: " .. result)
|
||||
player.print("ERROR: Serializing player data failed: " .. result)
|
||||
@@ -404,11 +414,11 @@ inventory_sync.events[defines.events.on_pre_player_left_game] = function(event)
|
||||
timeout = math.random(600, 1200), -- Start with 10-20 seconds for the timeout
|
||||
}
|
||||
|
||||
clusterio_api.send_json("inventory_sync_upload", result)
|
||||
clusterio_api.send_json("player_sync_upload", result)
|
||||
end
|
||||
|
||||
-- Invoked by the instance code to confirm the controller has received the uploaded data
|
||||
function inventory_sync.confirm_upload(player_name, generation)
|
||||
function player_sync.confirm_upload(player_name, generation)
|
||||
local player = game.get_player(player_name)
|
||||
if not player or player.connected then
|
||||
log("no player, or player connected")
|
||||
@@ -431,7 +441,7 @@ function inventory_sync.confirm_upload(player_name, generation)
|
||||
player_record.dirty = false
|
||||
end
|
||||
|
||||
function inventory_sync.initiate_inventory_download(player, player_record, generation)
|
||||
function player_sync.initiate_inventory_download(player, player_record, generation)
|
||||
player.print("Initiating inventory download...")
|
||||
local record = {
|
||||
started = game.ticks_played,
|
||||
@@ -448,7 +458,7 @@ function inventory_sync.initiate_inventory_download(player, player_record, gener
|
||||
recipe_notifications = serialize.serialize_crafting_notifications(player)
|
||||
end
|
||||
|
||||
clusterio_api.send_json("inventory_sync_download", {
|
||||
clusterio_api.send_json("player_sync_download", {
|
||||
player_name = player.name,
|
||||
recipe_notifications = recipe_notifications,
|
||||
})
|
||||
@@ -479,7 +489,7 @@ function inventory_sync.initiate_inventory_download(player, player_record, gener
|
||||
progress_dialog.display(player, 0, 1)
|
||||
end
|
||||
|
||||
function inventory_sync.check_active_downloads()
|
||||
function player_sync.check_active_downloads()
|
||||
-- Used for detecting the inventory download failing, like when the map is saved and
|
||||
-- re-loaded in the middle of a download. The player will be returned to a playable state.
|
||||
local script_data = get_script_data()
|
||||
@@ -503,7 +513,7 @@ function inventory_sync.check_active_downloads()
|
||||
end
|
||||
end
|
||||
|
||||
function inventory_sync.check_active_uploads()
|
||||
function player_sync.check_active_uploads()
|
||||
local script_data = get_script_data()
|
||||
for player_name, record in pairs(script_data.active_uploads) do
|
||||
if record.last_attempt + record.timeout < game.ticks_played then
|
||||
@@ -515,10 +525,10 @@ function inventory_sync.check_active_uploads()
|
||||
log("Retrying upload for " .. player_name)
|
||||
record.last_attempt = game.ticks_played
|
||||
record.timeout = math.random(record.timeout, record.timeout * 2)
|
||||
clusterio_api.send_json("inventory_sync_upload", record.serialized)
|
||||
clusterio_api.send_json("player_sync_upload", record.serialized)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return inventory_sync
|
||||
return player_sync
|
||||
File renamed without changes.
+3
-3
@@ -1,8 +1,8 @@
|
||||
local compat = require("modules/clusterio/compat")
|
||||
local clusterio_serialize = require("modules/clusterio/serialize")
|
||||
local character_inventories = require("modules/inventory_sync/define_player_inventories")
|
||||
local character_stat_keys = require("modules/inventory_sync/define_player_stat_keys")
|
||||
local game_view_settings_keys = require("modules/inventory_sync/define_game_view_settings")
|
||||
local character_inventories = require("modules/player_sync/define_player_inventories")
|
||||
local character_stat_keys = require("modules/player_sync/define_player_stat_keys")
|
||||
local game_view_settings_keys = require("modules/player_sync/define_game_view_settings")
|
||||
local serialize = {}
|
||||
|
||||
local v2_logistic_api = compat.version_ge("2.0.0")
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@clusterio/plugin-inventory_sync",
|
||||
"name": "@clusterio/plugin-player_sync",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"description": "Clusterio plugin forwarding between Factorio servers",
|
||||
"homepage": "https://github.com/clusterio/clusterio#readme",
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as mock from "../../../test/mock.js";
|
||||
import controllerEntrypoint from "../dist/node/controller.js";
|
||||
import { plugin as info } from "../dist/node/index.js";
|
||||
import { DatabaseStatsRequest } from "../dist/node/messages.js";
|
||||
|
||||
describe("player_sync controller", function() {
|
||||
const databaseDir = path.join("temp", "test", "player_sync");
|
||||
|
||||
before(function() {
|
||||
try {
|
||||
lib.registerPluginMessages([info]);
|
||||
} catch (err) {
|
||||
// Already registered by the full test suite
|
||||
}
|
||||
});
|
||||
beforeEach(async function() {
|
||||
await fs.rm(databaseDir, { recursive: true, force: true });
|
||||
await fs.mkdir(databaseDir, { recursive: true });
|
||||
});
|
||||
|
||||
async function load() {
|
||||
return await mock.loadControllerPlugin(async (context) => {
|
||||
context.controller.mockConfigEntries.set("controller.database_directory", databaseDir);
|
||||
await controllerEntrypoint(context);
|
||||
}, info);
|
||||
}
|
||||
|
||||
it("should move inventories.json to player_sync.json", async function() { // Alpha 28
|
||||
const playerData = { name: "test", generation: 1 };
|
||||
await fs.writeFile(path.join(databaseDir, "inventories.json"), JSON.stringify([["test", playerData]]));
|
||||
|
||||
const controller = await load();
|
||||
const stats = await mock.getHandler(controller, DatabaseStatsRequest)();
|
||||
assert.equal(stats.databaseEntries, 1);
|
||||
assert.equal(stats.largestEntry.name, "test");
|
||||
await assert.rejects(fs.access(path.join(databaseDir, "inventories.json")), { code: "ENOENT" });
|
||||
await fs.access(path.join(databaseDir, "player_sync.json"));
|
||||
});
|
||||
it("should not replace an existing player_sync.json", async function() {
|
||||
await fs.writeFile(path.join(databaseDir, "inventories.json"), JSON.stringify([["old", { name: "old" }]]));
|
||||
await fs.writeFile(path.join(databaseDir, "player_sync.json"), JSON.stringify([["new", { name: "new" }]]));
|
||||
|
||||
const controller = await load();
|
||||
const stats = await mock.getHandler(controller, DatabaseStatsRequest)();
|
||||
assert.equal(stats.databaseEntries, 1);
|
||||
assert.equal(stats.largestEntry.name, "new");
|
||||
});
|
||||
});
|
||||
@@ -19,7 +19,7 @@ async function decode(encoded) {
|
||||
return JSON.parse((await inflate(Buffer.from(encoded, "base64"))).toString());
|
||||
}
|
||||
|
||||
describe("inventory_sync", function() {
|
||||
describe("player_sync", function() {
|
||||
describe("recipeNotificationDelta()", function() {
|
||||
it("should be empty when the lists match", function() {
|
||||
assert.deepEqual(recipeNotificationDelta(["a", "b"], ["b", "a"]), {});
|
||||
@@ -71,7 +71,7 @@ describe("inventory_sync", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ipc-inventory_sync_download", function() {
|
||||
describe("ipc-player_sync_download", function() {
|
||||
let instance;
|
||||
before(async function() {
|
||||
try {
|
||||
@@ -80,7 +80,7 @@ describe("inventory_sync", function() {
|
||||
// Already registered by the full test suite
|
||||
}
|
||||
({ instance } = await mock.loadInstancePlugin(loadInstancePlugin, info));
|
||||
instance.mockConfigEntries.set("inventory_sync.rcon_chunk_size", 100000);
|
||||
instance.mockConfigEntries.set("player_sync.rcon_chunk_size", 100000);
|
||||
});
|
||||
beforeEach(function() {
|
||||
instance.server.reset();
|
||||
@@ -94,7 +94,7 @@ describe("inventory_sync", function() {
|
||||
});
|
||||
}
|
||||
async function download(request) {
|
||||
instance.server.emit("ipc-inventory_sync_download", request);
|
||||
instance.server.emit("ipc-player_sync_download", request);
|
||||
// zlib runs on the threadpool, so poll on timers rather than setImmediate
|
||||
for (let i = 0; i < 1000 && !instance.server.rconCommands.length; i++) {
|
||||
await lib.wait(1);
|
||||
@@ -102,7 +102,7 @@ describe("inventory_sync", function() {
|
||||
}
|
||||
function downloadCommand(playerData) {
|
||||
const json = lib.escapeString(JSON.stringify(playerData));
|
||||
return `/sc inventory_sync.download_inventory('test','${json}',1,1)`;
|
||||
return `/sc player_sync.download_inventory('test','${json}',1,1)`;
|
||||
}
|
||||
|
||||
it("should send an empty download when the controller has no data", async function() {
|
||||
@@ -110,7 +110,7 @@ describe("inventory_sync", function() {
|
||||
await download({ player_name: "test" });
|
||||
assert.deepEqual(
|
||||
instance.server.rconCommands,
|
||||
["/sc inventory_sync.download_inventory('test',nil,0,0)"]
|
||||
["/sc player_sync.download_inventory('test',nil,0,0)"]
|
||||
);
|
||||
});
|
||||
it("should send everything as a delta when no snapshot was given", async function() {
|
||||
@@ -0,0 +1,46 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { Controller } from "@clusterio/controller";
|
||||
|
||||
import { plugin as info } from "../dist/node/index.js";
|
||||
|
||||
describe("player_sync migrations", function() {
|
||||
before(async function() {
|
||||
lib.addPluginConfigFields([info]);
|
||||
const packagePath = fileURLToPath(new URL("../package.json", import.meta.url));
|
||||
await lib.loadPluginMigrations([{ ...info, packagePath }]);
|
||||
});
|
||||
after(function() {
|
||||
for (const hook of [
|
||||
lib.ControllerConfig.pluginMigrations, lib.HostConfig.pluginMigrations,
|
||||
lib.InstanceConfig.pluginMigrations, lib.Role.pluginMigrations,
|
||||
]) {
|
||||
hook.detach(info.name);
|
||||
}
|
||||
});
|
||||
|
||||
it("should rename inventory_sync config fields", function() { // Alpha 28
|
||||
const controllerConfig = lib.ControllerConfig.fromJSON({
|
||||
"inventory_sync.load_plugin": false,
|
||||
"inventory_sync.player_lock_timeout": 120,
|
||||
}, "controller");
|
||||
assert.equal(controllerConfig.get("player_sync.load_plugin"), false);
|
||||
assert.equal(controllerConfig.get("player_sync.player_lock_timeout"), 120);
|
||||
assert.equal(controllerConfig.toJSON()["inventory_sync.player_lock_timeout"], undefined);
|
||||
|
||||
const hostConfig = lib.HostConfig.fromJSON({ "inventory_sync.load_plugin": false }, "host");
|
||||
assert.equal(hostConfig.get("player_sync.load_plugin"), false);
|
||||
|
||||
const instanceConfig = lib.InstanceConfig.fromJSON({
|
||||
"inventory_sync.rcon_chunk_size": 500,
|
||||
}, "controller");
|
||||
assert.equal(instanceConfig.get("player_sync.rcon_chunk_size"), 500);
|
||||
});
|
||||
it("should rename the inventory_sync permission in roles", function() { // Alpha 28
|
||||
const result = Controller.migrateRoles([
|
||||
{ permissions: ["core.admin", "inventory_sync.inventory.view"] },
|
||||
]);
|
||||
assert.deepEqual(result, [{ permissions: ["core.admin", "player_sync.inventory.view"] }]);
|
||||
});
|
||||
});
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -17,8 +17,8 @@ function InventoryPage() {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <PageLayout nav={[{ name: "Inventory sync" }]}>
|
||||
<PageHeader title="Inventory sync" />
|
||||
return <PageLayout nav={[{ name: "Player sync" }]}>
|
||||
<PageHeader title="Player sync" />
|
||||
{statsData && <>
|
||||
<p>Database size: {Math.round(statsData.databaseSize / 1000)}kB</p>
|
||||
<p>Database entries: {statsData.databaseEntries}</p>
|
||||
@@ -33,8 +33,8 @@ export default async function(context: WebPluginContext) {
|
||||
control.hooks.pages.attach(plugin.name, () => [
|
||||
{
|
||||
path: "/inventory",
|
||||
sidebarName: "Inventory sync",
|
||||
permission: "inventory_sync.inventory.view",
|
||||
sidebarName: "Player sync",
|
||||
permission: "player_sync.inventory.view",
|
||||
content: <InventoryPage />,
|
||||
},
|
||||
]);
|
||||
File renamed without changes.
@@ -13,8 +13,8 @@ module.exports = (env = {}, argv = {}) => merge(common(env, argv), {
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "inventory_sync",
|
||||
library: { type: "var", name: "plugin_inventory_sync" },
|
||||
name: "player_sync",
|
||||
library: { type: "var", name: "plugin_player_sync" },
|
||||
exposes: {
|
||||
"./": "./index.ts",
|
||||
"./package.json": "./package.json",
|
||||
@@ -9,13 +9,13 @@ describe("ctl/config_ops", function() {
|
||||
"# Clusterio configuration",
|
||||
"",
|
||||
"instance.name = Example",
|
||||
"inventory_sync.load_plugin = false",
|
||||
"player_sync.load_plugin = false",
|
||||
"",
|
||||
].join(newline);
|
||||
|
||||
assert.deepEqual(await configToKeyVal(data), {
|
||||
"instance.name": "Example",
|
||||
"inventory_sync.load_plugin": "false",
|
||||
"player_sync.load_plugin": "false",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -909,7 +909,7 @@ describe("Integration of Clusterio", function() {
|
||||
describe("instance send-rcon", function() {
|
||||
requiresFactorio(this);
|
||||
const plugins = [
|
||||
"global_chat", "inventory_sync", "player_auth",
|
||||
"global_chat", "player_sync", "player_auth",
|
||||
"research_sync", "statistics_exporter", "subspace_storage",
|
||||
];
|
||||
this.beforeAll(async function() {
|
||||
@@ -1276,7 +1276,7 @@ describe("Integration of Clusterio", function() {
|
||||
await execCtl("instance config set 44 research_sync.load_plugin false");
|
||||
await execCtl("instance config set 44 statistics_exporter.load_plugin false");
|
||||
await execCtl("instance config set 44 subspace_storage.load_plugin false");
|
||||
await execCtl("instance config set 44 inventory_sync.load_plugin false");
|
||||
await execCtl("instance config set 44 player_sync.load_plugin false");
|
||||
await execCtl("instance config set 44 factorio.enable_save_patching false");
|
||||
|
||||
await execCtl("instance start 44");
|
||||
|
||||
@@ -345,7 +345,7 @@ before(async function() {
|
||||
await execCtlProcess("plugin add ../../plugins/research_sync");
|
||||
await execCtlProcess("plugin add ../../plugins/statistics_exporter");
|
||||
await execCtlProcess("plugin add ../../plugins/subspace_storage");
|
||||
await execCtlProcess("plugin add ../../plugins/inventory_sync");
|
||||
await execCtlProcess("plugin add ../../plugins/player_sync");
|
||||
await execCtlProcess("plugin add ../../plugins/player_auth");
|
||||
|
||||
console.log("Bootstrapping");
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("Clusterio Instance", function() {
|
||||
await execCtl(`instance config set ${instName} instance.exclude_from_start_all true`);
|
||||
|
||||
for (const plugin of [
|
||||
"global_chat", "research_sync", "statistics_exporter", "subspace_storage", "player_auth", "inventory_sync",
|
||||
"global_chat", "research_sync", "statistics_exporter", "subspace_storage", "player_auth", "player_sync",
|
||||
]) {
|
||||
await execCtl(`instance config set ${instName} ${plugin}.load_plugin false`);
|
||||
}
|
||||
@@ -255,7 +255,7 @@ describe("start-all and stop-all commands", function() {
|
||||
for (const instanceName of [testInstName1, testInstName2, testInstName3]) {
|
||||
for (const plugin of [
|
||||
"global_chat", "research_sync", "statistics_exporter",
|
||||
"subspace_storage", "player_auth", "inventory_sync",
|
||||
"subspace_storage", "player_auth", "player_sync",
|
||||
]) {
|
||||
await execCtl(`instance config set ${instanceName} ${plugin}.load_plugin false`);
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
{ "path": "./packages/ctl/" },
|
||||
{ "path": "./packages/web_ui/" },
|
||||
{ "path": "./plugins/global_chat/" },
|
||||
{ "path": "./plugins/inventory_sync/" },
|
||||
{ "path": "./plugins/player_sync/" },
|
||||
{ "path": "./plugins/player_auth/" },
|
||||
{ "path": "./plugins/research_sync/" },
|
||||
{ "path": "./plugins/statistics_exporter/" },
|
||||
|
||||
Reference in new issue
Block a user