First working draft of inventory sync

This commit is contained in:
Daniel committed 2021-07-12 22:05:53 +02:00
1 parent a578719fc5
commit 86bd07666f
12 files changed
+512

No files matched your search

+1
View File
@@ -0,0 +1 @@
webpack.config.js
+12
View File
@@ -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.
+30
View File
@@ -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,
};
+120
View File
@@ -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" },
},
},
},
}),
},
};
+87
View File
@@ -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,
};
+78
View File
@@ -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"]
}
+32
View File
@@ -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.
+32
View File
@@ -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") {
}
}
}
+34
View File
@@ -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 },
},
}),
],
});