From 08e7434d247c0e5ef1644076591f19bca800c986 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Wed, 13 Jul 2022 00:28:25 +0200 Subject: [PATCH] Add command to extract play time from save Add command that extracts play time (and players) from the currently running save to the recorded instance statistics. This is useful for getting correct play time statistics when an existing save game is imported. --- docs/managing-a-cluster.md | 10 +++++++ packages/ctl/src/commands.js | 13 +++++++++ packages/lib/link/messages.js | 9 +++++- packages/lib/plugin.js | 4 +-- packages/lib/users.js | 5 ++++ packages/slave/src/Instance.js | 28 +++++++++++++++++++ .../src/components/InstanceViewPage.jsx | 13 +++++++++ test/integration/clusterio.js | 7 +++++ 8 files changed, 86 insertions(+), 3 deletions(-) diff --git a/docs/managing-a-cluster.md b/docs/managing-a-cluster.md index dca36cff..28c03a39 100644 --- a/docs/managing-a-cluster.md +++ b/docs/managing-a-cluster.md @@ -50,6 +50,16 @@ Terminates the running Factorio server without giving it a chance to save or cle Note: This may cause loss of data. +### Extract players from instance + + ctl> instance extract-players + +Creates a user account in the cluster for each player that has been online on the currently running save of the instance and sets the play time of that account on this instance to the online time recorded in the save. + +Useful when importing a save to a cluster. +Note that the online time in a save is recorded differntly from how Clusterio records online time. +Most notably Clusterio records wall clock time while the save time is recorded in ticks and this extraction of player time assumes the game runs at 60 ticks a second. + ### Config management diff --git a/packages/ctl/src/commands.js b/packages/ctl/src/commands.js index 7c7fa270..06f83956 100644 --- a/packages/ctl/src/commands.js +++ b/packages/ctl/src/commands.js @@ -748,6 +748,19 @@ instanceCommands.add(new libCommand.Command({ }, })); +instanceCommands.add(new libCommand.Command({ + definition: ["extract-players ", "Extract players from running save into the cluster.", (yargs) => { + yargs.positional("instance", { describe: "Instance to extract players and play time from", type: "string" }); + }], + handler: async function(args, control) { + let instanceId = await libCommand.resolveInstance(control, args.instance); + await control.setLogSubscriptions({ instance_ids: [instanceId] }); + await libLink.messages.extractPlayers.send(control, { + instance_id: instanceId, + }); + }, +})); + instanceCommands.add(new libCommand.Command({ definition: ["start ", "Start instance", (yargs) => { yargs.positional("instance", { describe: "Instance to start", type: "string" }); diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js index 6028c31e..cd0a00c8 100644 --- a/packages/lib/link/messages.js +++ b/packages/lib/link/messages.js @@ -668,6 +668,13 @@ messages.exportData = new Request({ forwardTo: "instance", }); +messages.extractPlayers = new Request({ + type: "extract_players", + links: ["control-master", "master-slave", "slave-instance"], + permission: "core.instance.extract_players", + forwardTo: "instance", +}); + messages.stopInstance = new Request({ type: "stop_instance", links: ["control-master", "master-slave", "slave-instance"], @@ -1331,7 +1338,7 @@ messages.playerEvent = new Event({ eventRequired: ["instance_id", "type", "name"], eventProperties: { "instance_id": { type: "integer" }, - "type": { type: "string", enum: ["join", "leave"] }, + "type": { type: "string", enum: ["join", "leave", "import"] }, "name": { type: "string" }, "reason": { type: "string" }, "stats": PlayerStats.jsonSchema, diff --git a/packages/lib/plugin.js b/packages/lib/plugin.js index 162794d4..9a1cc25f 100644 --- a/packages/lib/plugin.js +++ b/packages/lib/plugin.js @@ -206,7 +206,7 @@ class BaseInstancePlugin { * Invoked when a player either joins or leaves the instance. * * @param {Object} event - Information about the event. - * @param {string} event.type - Either "join" or "leave". + * @param {string} event.type - Either "join", "leave" or "import". * @param {string} event.name - Name of the player that joined/left. * @param {string=} event.reason - * Only present for type "leave". Reason for player leaving the @@ -502,7 +502,7 @@ class BaseMasterPlugin { * * @param {Object} instance - the instance it occured on. * @param {Object} event - Information about the event. - * @param {string} event.type - Either "join" or "leave". + * @param {string} event.type - Either "join", "leave" or "import". * @param {string} event.name - Name of the player that joined/left. * @param {string=} event.reason - * Only present for type "leave". Reason for player leaving the diff --git a/packages/lib/users.js b/packages/lib/users.js index c1f72afd..2ccc702b 100644 --- a/packages/lib/users.js +++ b/packages/lib/users.js @@ -475,6 +475,11 @@ definePermission({ title: "Export locale and icons", description: "Export the the locale and icons from an instance and upload it to the master.", }); +definePermission({ + name: "core.instance.extract_players", + title: "Extract player stats from running save", + description: "Run extraction to create a user for each player in the save and set the play time from the save.", +}); definePermission({ name: "core.instance.start", title: "Start instance", diff --git a/packages/slave/src/Instance.js b/packages/slave/src/Instance.js index a3b6fb6a..a5a717a6 100644 --- a/packages/slave/src/Instance.js +++ b/packages/slave/src/Instance.js @@ -370,6 +370,34 @@ class Instance extends libLink.Link { libPlugin.invokeHook(this.plugins, "onPlayerEvent", event); } + async extractPlayersRequestHandler() { + const exportPlayerTimes = `/sc +local players = {} +for _, p in pairs(game.players) do + players[p.name] = p.online_time +end +rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); + let playerTimes = JSON.parse(await this.sendRcon(exportPlayerTimes)); + + for (let [name, onlineTimeTicks] of Object.entries(playerTimes)) { + let stats = this.playerStats.get(name); + if (!stats) { + stats = new PlayerStats(); + this.playerStats.set(name, stats); + } + stats.onlineTimeMs = onlineTimeTicks * 1000 / 60; + + let event = { + instance_id: this.id, + type: "import", + name, + stats: stats.toJSON(), + }; + libLink.messages.playerEvent.send(this, event); + libPlugin.invokeHook(this.plugins, "onPlayerEvent", event); + } + } + async sendRcon(message, expectEmpty, plugin = "") { let instanceId = String(this.id); let observeDuration = instanceRconCommandDuration.labels(instanceId).startTimer(); diff --git a/packages/web_ui/src/components/InstanceViewPage.jsx b/packages/web_ui/src/components/InstanceViewPage.jsx index e544bb60..e0eb3ff3 100644 --- a/packages/web_ui/src/components/InstanceViewPage.jsx +++ b/packages/web_ui/src/components/InstanceViewPage.jsx @@ -100,6 +100,13 @@ export default function InstanceViewPage(props) { label: "Export data", }); } + if (account.hasPermission("core.instance.extract_players")) { + instanceButtonMenuItems.push({ + disabled: instance["status"] !== "running", + key: "extract", + label: "Extract players from save", + }); + } if (account.hasPermission("core.instance.kill")) { instanceButtonMenuItems.push({ disabled: ["unknown", "unassigned", "stopped"].includes(instance["status"]), @@ -132,6 +139,11 @@ export default function InstanceViewPage(props) { setExportingData(false); }); + } else if (key === "extract") { + libLink.messages.extractPlayers.send( + control, { instance_id: instanceId } + ).catch(notifyErrorHandler("Error extracting player data")); + } else if (key === "kill") { libLink.messages.killInstance.send( control, { instance_id: instanceId } @@ -162,6 +174,7 @@ export default function InstanceViewPage(props) { {account.hasPermission("core.instance.load_scenario") && } {account.hasAnyPermission( "core.instance.export_data", + "core.instance.extract_players", "core.instance.kill", "core.instance.delete", ) && diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index aab1af97..375c1eca 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -524,6 +524,13 @@ describe("Integration of Clusterio", function() { }); }); + describe("instance extract-players", function() { + it("runs", async function() { + slowTest(this); + await execCtl("instance extract-players test"); + }); + }); + describe("instance stop", function() { it("stops the instance", async function() { slowTest(this);