mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 07:24:52 +09:00
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:
1 parent
0245ccd16c
commit
08e7434d24
8 files changed
+86
-3
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user