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.
This commit is contained in:
Hornwitser committed 2022-07-13 00:31:02 +02:00
1 parent 0245ccd16c
commit 08e7434d24
8 files changed
+86 -3

No files matched your search

+10
View File
@@ -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 <name>
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
+13
View File
@@ -748,6 +748,19 @@ instanceCommands.add(new libCommand.Command({
},
}));
instanceCommands.add(new libCommand.Command({
definition: ["extract-players <instance>", "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 <instance>", "Start instance", (yargs) => {
yargs.positional("instance", { describe: "Instance to start", type: "string" });
+8 -1
View File
@@ -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,
+2 -2
View File
@@ -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
+5
View File
@@ -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",
+28
View File
@@ -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();
@@ -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") && <LoadScenarioModal instance={instance} />}
{account.hasAnyPermission(
"core.instance.export_data",
"core.instance.extract_players",
"core.instance.kill",
"core.instance.delete",
) && <Dropdown placement="bottomRight" trigger={["click"]} overlay={instanceButtonsMenu}>
+7
View File
@@ -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);