mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 07:04:52 +09:00
First working draft of inventory sync
This commit is contained in:
1 parent
a578719fc5
commit
86bd07666f
12 files changed
+512
No files matched your search
@@ -0,0 +1 @@
|
||||
webpack.config.js
|
||||
@@ -0,0 +1,12 @@
|
||||
# Clusterio inventory sync plugin
|
||||
|
||||
Carry over player inventory between servers
|
||||
|
||||
## Installation
|
||||
|
||||
Run the following commands in the folder Clusterio is installed to:
|
||||
|
||||
npm install @clusterio/plugin-inventory_sync
|
||||
npx clusteriomaster plugin add @clusterio/plugin-inventory_sync
|
||||
|
||||
Substitute clusteriomaster with clusterioslave or clusterioctl if this a dedicate slave or ctl installation respectively.
|
||||
@@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const info = require("./info");
|
||||
const libCommand = require("@clusterio/lib/command");
|
||||
|
||||
|
||||
const globalChatCommands = new libCommand.CommandTree({
|
||||
name: "global-chat", description: "Global Chat plugin commands",
|
||||
});
|
||||
globalChatCommands.add(new libCommand.Command({
|
||||
definition: ["shout <message>", "Send message to all instances", (yargs) => {
|
||||
yargs.positional("message", { describe: "message to send", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
await info.messages.chat.send(control, {
|
||||
instance_name: "Console",
|
||||
content: args.message,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
class ControlPlugin extends libPlugin.BaseControlPlugin {
|
||||
async addCommands(rootCommand) {
|
||||
rootCommand.add(globalChatCommands);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ControlPlugin,
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
"use strict";
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
const libUsers = require("@clusterio/lib/users");
|
||||
|
||||
class MasterConfigGroup extends libConfig.PluginConfigGroup { }
|
||||
MasterConfigGroup.defaultAccess = ["master", "slave", "control"];
|
||||
MasterConfigGroup.groupName = "inventory_sync";
|
||||
MasterConfigGroup.define({
|
||||
name: "autosave_interval",
|
||||
title: "Autosave Interval",
|
||||
description: "Interval the player data is autosaved at in seconds.",
|
||||
type: "number",
|
||||
initial_value: 60,
|
||||
});
|
||||
MasterConfigGroup.define({
|
||||
name: "spectator_until_ready",
|
||||
title: "Spectator mode until ready",
|
||||
description: "Set the player into spectator mode until the inventory is fully synchronized.",
|
||||
type: "boolean",
|
||||
initial_value: true,
|
||||
});
|
||||
MasterConfigGroup.define({
|
||||
name: "clear_old_inventory",
|
||||
title: "Clear old inventory on join",
|
||||
description: "Clear old inventory on join to prevent duplication of items. Can cause greater disruption on failed or slow sync.",
|
||||
type: "boolean",
|
||||
initial_value: true,
|
||||
});
|
||||
MasterConfigGroup.finalize();
|
||||
|
||||
class InstanceConfigGroup extends libConfig.PluginConfigGroup { }
|
||||
InstanceConfigGroup.defaultAccess = ["master", "slave", "control"];
|
||||
InstanceConfigGroup.groupName = "inventory_sync";
|
||||
InstanceConfigGroup.define({
|
||||
name: "enable_inventory_sync",
|
||||
title: "Syncronize inventories",
|
||||
description: "Syncronize inventories. Disable if this instance is running incompatible mods or otherwise shouldn't sync.",
|
||||
type: "boolean",
|
||||
initial_value: true,
|
||||
});
|
||||
InstanceConfigGroup.finalize();
|
||||
|
||||
libUsers.definePermission({
|
||||
name: "inventory_sync.inventory.view",
|
||||
title: "View player inventories",
|
||||
description: "View player inventories",
|
||||
grantByDefault: true,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
name: "inventory_sync",
|
||||
title: "Inventory sync",
|
||||
description: "Synchronizes players inventories between instances",
|
||||
|
||||
instanceEntrypoint: "instance",
|
||||
InstanceConfigGroup,
|
||||
|
||||
masterEntrypoint: "master",
|
||||
MasterConfigGroup,
|
||||
|
||||
webEntrypoint: "./web",
|
||||
routes: ["/inventory"],
|
||||
|
||||
messages: {
|
||||
chat: new libLink.Event({
|
||||
type: "inventory_sync:chat",
|
||||
links: ["instance-slave", "slave-master", "master-slave", "slave-instance"],
|
||||
forwardTo: "master",
|
||||
broadcastTo: "instance",
|
||||
eventProperties: {
|
||||
"instance_name": { type: "string" },
|
||||
"content": { type: "string" },
|
||||
},
|
||||
}),
|
||||
upload: new libLink.Request({
|
||||
type: "inventory_sync:upload",
|
||||
links: ["instance-slave", "slave-master"],
|
||||
forwardTo: "master",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"instance_name": { type: "string" },
|
||||
"player_name": { type: "string" },
|
||||
"inventory": { type: "string" },
|
||||
},
|
||||
responseProperties: {
|
||||
"success": { type: "boolean" },
|
||||
},
|
||||
}),
|
||||
download: new libLink.Request({
|
||||
type: "inventory_sync:download",
|
||||
links: ["instance-slave", "slave-master"],
|
||||
forwardTo: "master",
|
||||
requestProperties: {
|
||||
"player_name": { type: "string" },
|
||||
},
|
||||
responseProperties: {
|
||||
"player_name": { type: "string" },
|
||||
"inventory": { type: "string" },
|
||||
},
|
||||
}),
|
||||
databaseStats: new libLink.Request({
|
||||
type: "inventory_sync:databaseStats",
|
||||
links: ["control-master"],
|
||||
permission: "inventory_sync.inventory.view",
|
||||
requestProperties: {},
|
||||
responseProperties: {
|
||||
"database_size": { type: "integer" },
|
||||
"database_entries": { type: "integer" },
|
||||
"largest_entry": {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
size: { type: "number" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @module
|
||||
*/
|
||||
"use strict";
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const libLuaTools = require("@clusterio/lib/lua_tools");
|
||||
|
||||
class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
async init() {
|
||||
this.inventoryQueue = [];
|
||||
|
||||
// setInterval(() => {
|
||||
|
||||
// console.log("Connected")
|
||||
// this.info.messages.upload.send(this.instance, {
|
||||
// instance_id: this.instance.id,
|
||||
// player: "danielv",
|
||||
// inventory: "stuff",
|
||||
// })
|
||||
// },5000)
|
||||
|
||||
|
||||
this.instance.server.on("ipc-inventory_sync_upload", content =>
|
||||
this.handleUpload(content).catch(err => this.logger.error(
|
||||
`Error handling ipc-inventory_sync_upload:\n${err.stack}`
|
||||
))
|
||||
)
|
||||
this.instance.server.on("ipc-inventory_sync_download", content =>
|
||||
this.handleDownload(content).catch(err => this.logger.error(
|
||||
`Error handling ipc-inventory_sync_download:\n${err.stack}`
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
onMasterConnectionEvent(event) {
|
||||
console.log("Connectionevent", event)
|
||||
if (event === "connect") {
|
||||
}
|
||||
}
|
||||
|
||||
async handleUpload(player) {
|
||||
console.log("Uploading", player)
|
||||
this.info.messages.upload.send(this.instance, {
|
||||
instance_id: this.instance.id,
|
||||
instance_name: this.instance.name,
|
||||
player_name: player.name,
|
||||
inventory: JSON.stringify(player),
|
||||
})
|
||||
}
|
||||
async handleDownload(player) {
|
||||
console.log("Downloading", player.player_name)
|
||||
let response = await this.info.messages.download.send(this.instance, {
|
||||
player_name: player.player_name
|
||||
})
|
||||
if (response.inventory) {
|
||||
console.log("Sending command")
|
||||
await this.sendRcon(`/sc inventory_sync.downloadInventory('${response.player_name}', '${libLuaTools.escapeString(response.inventory)}')`)
|
||||
}
|
||||
}
|
||||
|
||||
async chatEventHandler(message) {
|
||||
// TODO check if cross server chat is enabled
|
||||
let content = `[${message.data.instance_name}] ${removeTags(message.data.content)}`;
|
||||
await this.sendRcon(`/sc game.print('${libLuaTools.escapeString(content)}')`, true);
|
||||
}
|
||||
|
||||
sendChat(message) {
|
||||
this.info.messages.chat.send(this.instance, {
|
||||
instance_name: this.instance.name,
|
||||
content: message,
|
||||
});
|
||||
}
|
||||
|
||||
async onOutput(output) {
|
||||
if (output.type === "action" && output.action === "INVENTORY") {
|
||||
if (this.slave.connector.connected) {
|
||||
this.sendChat(output.message);
|
||||
} else {
|
||||
this.inventoryQueue.push(output.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
InstancePlugin,
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
"use strict";
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
|
||||
async function loadDatabase(config, logger) {
|
||||
let itemsPath = path.resolve(config.get("master.database_directory"), "inventories.json");
|
||||
logger.verbose(`Loading ${itemsPath}`);
|
||||
try {
|
||||
let content = await fs.readFile(itemsPath);
|
||||
return JSON.parse(content);
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
logger.verbose("Creating new inventory database");
|
||||
return {};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDatabase(masterConfig, inventories, logger) {
|
||||
if (inventories) {
|
||||
let file = path.resolve(masterConfig.get("master.database_directory"), "inventories.json");
|
||||
logger.verbose(`writing ${file}`);
|
||||
let content = JSON.stringify(inventories);
|
||||
await fs.outputFile(file, content);
|
||||
}
|
||||
}
|
||||
|
||||
class MasterPlugin extends libPlugin.BaseMasterPlugin {
|
||||
async init() {
|
||||
this.inventories = await loadDatabase(this.master.config, this.logger);
|
||||
this.autosaveId = setInterval(() => {
|
||||
saveDatabase(this.master.config, this.inventories, this.logger).catch(err => {
|
||||
this.logger.error(`Unexpected error autosaving inventories:\n${err.stack}`);
|
||||
});
|
||||
}, this.master.config.get("inventory_sync.autosave_interval") * 1000);
|
||||
}
|
||||
async uploadRequestHandler(message) {
|
||||
console.log("Saving inventory for", message.data.player_name)
|
||||
this.inventories[message.data.player_name] = message.data.inventory
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
async downloadRequestHandler(message) {
|
||||
console.log("Downloading inventory for", message.data.player_name)
|
||||
return {
|
||||
player_name: message.data.player_name,
|
||||
inventory: this.inventories[message.data.player_name]
|
||||
}
|
||||
}
|
||||
async onShutdown() {
|
||||
clearInterval(this.autosaveId);
|
||||
await saveDatabase(this.master.config, this.inventories, this.logger);
|
||||
}
|
||||
async databaseStatsRequestHandler(message) {
|
||||
let inventories = Object.keys(this.inventories)
|
||||
.map(name => ({
|
||||
name,
|
||||
length: this.inventories[name].length
|
||||
}))
|
||||
.sort((a, b) => a.stringified.length - b.stringified.length)
|
||||
return {
|
||||
database_size: inventories.reduce((a, b) => b - a, 0),
|
||||
database_entries: inventories.length,
|
||||
largest_entry: {
|
||||
name: inventories[0].name,
|
||||
size: inventories[0].length,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MasterPlugin,
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
--[[
|
||||
|
||||
When a player leaves the game, serialize their inventory and upload it to the master.
|
||||
|
||||
When a player joins the game, send a request to the master server for the players inventory.
|
||||
|
||||
When the master sends an inventory, parse it and give it to the player.
|
||||
|
||||
|
||||
packages/slave/modules/clusterio
|
||||
]]
|
||||
|
||||
local serialize = require("modules/clusterio/serialize")
|
||||
local clusterio_api = require("modules/clusterio/api")
|
||||
|
||||
local inventories = {
|
||||
defines.inventory.character_main,
|
||||
defines.inventory.character_guns,
|
||||
defines.inventory.character_ammo,
|
||||
defines.inventory.character_armor,
|
||||
defines.inventory.character_vehicle,
|
||||
defines.inventory.character_trash,
|
||||
}
|
||||
|
||||
inventory_sync = {}
|
||||
|
||||
inventory_sync.events = {}
|
||||
-- Download inventory from master
|
||||
inventory_sync.events[defines.events.on_player_joined_game] = function(event)
|
||||
local playerIndex = event.player_index
|
||||
inventory_sync.initiateInventoryDownload(playerIndex)
|
||||
end
|
||||
|
||||
function inventory_sync.initiateInventoryDownload(playerIndex)
|
||||
game.players[playerIndex].print("Initiating inventory download...")
|
||||
global["inv_sync_download_start_tick "..game.players[playerIndex].name] = game.tick
|
||||
clusterio_api.send_json("inventory_sync_download", {
|
||||
player_name = game.players[playerIndex].name
|
||||
})
|
||||
end
|
||||
|
||||
-- This function is called by instance.js as /sc inventory_sync.downloadInventory("danielv", Escaped JSON string) with data from the master
|
||||
function inventory_sync.downloadInventory(playerName, data)
|
||||
local serialized_player = game.json_to_table(data)
|
||||
local player = game.players[playerName]
|
||||
-- game.print(serpent.block(serialized_player))
|
||||
for _, inv in pairs(inventories) do
|
||||
local inventory = player.get_inventory(inv)
|
||||
if inventory ~= nil and serialized_player.inventories[tostring(inv)] ~= nil then
|
||||
serialize.deserialize_inventory(inventory, serialized_player.inventories[tostring(inv)])
|
||||
end
|
||||
end
|
||||
local startTick = global["inv_sync_download_start_tick "..game.players[playerName].name]
|
||||
game.print("Imported inventory for "..playerName.." in "..game.tick - startTick.." ticks")
|
||||
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_player_left_game] = function(event)
|
||||
local playerIndex = event.player_index
|
||||
|
||||
inventory_sync.uploadInventory(playerIndex)
|
||||
end
|
||||
|
||||
function inventory_sync.uploadInventory(playerIndex)
|
||||
local player = game.players[playerIndex]
|
||||
|
||||
local serialized_player = {
|
||||
name = player.name,
|
||||
inventories = {}
|
||||
}
|
||||
|
||||
for _, inv in pairs(inventories) do
|
||||
local inventory = player.get_inventory(inv)
|
||||
if inventory ~= nil then
|
||||
local serialized_inventory = serialize.serialize_inventory(inventory)
|
||||
serialized_player.inventories[inv] = serialized_inventory
|
||||
end
|
||||
end
|
||||
clusterio_api.send_json("inventory_sync_upload", serialized_player)
|
||||
end
|
||||
|
||||
return inventory_sync
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "inventory_sync",
|
||||
"load": ["inventory_sync.lua"]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@clusterio/plugin-inventory_sync",
|
||||
"version": "2.0.0-alpha.6",
|
||||
"description": "Clusterio plugin forwarding between Factorio servers",
|
||||
"author": "Danielv <danielv@danielv.no>",
|
||||
"homepage": "https://github.com/clusterio/factorioClusterio#readme",
|
||||
"license": "MIT",
|
||||
"repository": "clusterio/factorioClusterio",
|
||||
"scripts": {
|
||||
"build": "webpack-cli --env production",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/clusterio/factorioClusterio/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.6",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.6",
|
||||
"webpack": "^5.36.2",
|
||||
"webpack-cli": "^4.7.0",
|
||||
"webpack-merge": "^5.2.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,32 @@
|
||||
import React, { useContext } from "react";
|
||||
import { Typography } from "antd";
|
||||
|
||||
import libPlugin from "@clusterio/lib/plugin";
|
||||
import { notifyErrorHandler, PageLayout, ControlContext } from "@clusterio/web_ui";
|
||||
import info from "../info";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
function InventoryPage() {
|
||||
// let control = useContext(ControlContext);
|
||||
|
||||
return <PageLayout nav={[{ name: "Inventory sync" }]}>
|
||||
<h2>Inventory sync</h2>
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
export class WebPlugin extends libPlugin.BaseWebPlugin {
|
||||
async init() {
|
||||
this.pages = [
|
||||
{ path: "/inventory", sidebarName: "Inventory sync", content: <InventoryPage /> },
|
||||
];
|
||||
}
|
||||
|
||||
onMasterConnectionEvent(event) {
|
||||
if (event === "connect") {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const common = require("@clusterio/web_ui/webpack.common");
|
||||
|
||||
module.exports = (env = {}) => merge(common(env), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.jsx",
|
||||
output: {
|
||||
publicPath: "auto",
|
||||
filename: "bundle.js",
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "inventory_sync",
|
||||
library: { type: "var", name: "plugin_inventory_sync" },
|
||||
filename: "remoteEntry.js",
|
||||
exposes: {
|
||||
"./info": "./info.js",
|
||||
"./package.json": "./package.json",
|
||||
"./web": "./web/index.jsx",
|
||||
},
|
||||
shared: {
|
||||
"@clusterio/lib/config": { import: false },
|
||||
"@clusterio/lib/link": { import: false },
|
||||
"@clusterio/web_ui": { import: false },
|
||||
"ajv": { import: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
Reference in new issue
Block a user