mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 15:24:52 +09:00
Convert global_chat, research_sync and statistics_exporter to entrypoint functions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bd609c3c54
commit
cfeb4b6a4a
7 files changed
+231
-237
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { CommandTree, Command } from "@clusterio/lib";
|
||||
import { BaseCtlPlugin } from "@clusterio/ctl";
|
||||
import type { CtlPluginContext } from "@clusterio/ctl";
|
||||
import { ChatEvent } from "./messages.js";
|
||||
|
||||
const globalChatCommands = new CommandTree({
|
||||
@@ -14,8 +14,8 @@ globalChatCommands.add(new Command({
|
||||
},
|
||||
}));
|
||||
|
||||
export class CtlPlugin extends BaseCtlPlugin {
|
||||
async addCommands(rootCommand: CommandTree) {
|
||||
export default async function(context: CtlPluginContext) {
|
||||
context.hooks.addCommands.attach(context.plugin.name, async (rootCommand) => {
|
||||
rootCommand.add(globalChatCommands);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
import type { InstancePluginContext } from "@clusterio/host";
|
||||
import { ChatEvent } from "./messages.js";
|
||||
|
||||
/**
|
||||
@@ -12,41 +12,38 @@ function removeTags(content: string): string {
|
||||
return content.replace(/\[(?:gps|special-item|train|train-stop)=\S*?\]/gm, "");
|
||||
}
|
||||
|
||||
export class InstancePlugin extends BaseInstancePlugin {
|
||||
messageQueue: string[] = [];
|
||||
export default async function(context: InstancePluginContext) {
|
||||
const { instance, host, plugin } = context;
|
||||
let messageQueue: string[] = [];
|
||||
|
||||
async init() {
|
||||
this.instance.handle(ChatEvent, this.handleChatEvent.bind(this));
|
||||
function sendChat(message: string) {
|
||||
instance.sendTo("allInstances", new ChatEvent(instance.name, message));
|
||||
}
|
||||
|
||||
onControllerConnectionEvent(event: string) {
|
||||
if (event === "connect") {
|
||||
for (let message of this.messageQueue) {
|
||||
this.sendChat(message);
|
||||
}
|
||||
this.messageQueue = [];
|
||||
}
|
||||
}
|
||||
|
||||
async handleChatEvent(event: ChatEvent) {
|
||||
instance.handle(ChatEvent, async (event: ChatEvent) => {
|
||||
// TODO check if cross server chat is enabled
|
||||
let content = `[${event.instanceName}] ${removeTags(event.content)}`;
|
||||
await this.sendRcon(`/sc game.print('${lib.escapeString(content)}')`, true);
|
||||
}
|
||||
await instance.sendRcon(`/sc game.print('${lib.escapeString(content)}')`, true, plugin.name);
|
||||
});
|
||||
|
||||
sendChat(message: string) {
|
||||
this.instance.sendTo("allInstances", new ChatEvent(this.instance.name, message));
|
||||
}
|
||||
instance.hooks.controllerConnectionEvent.attach(plugin.name, (event) => {
|
||||
if (event === "connect") {
|
||||
for (let message of messageQueue) {
|
||||
sendChat(message);
|
||||
}
|
||||
messageQueue = [];
|
||||
}
|
||||
});
|
||||
|
||||
async onOutput(output: lib.ParsedFactorioOutput) {
|
||||
instance.hooks.output.attach(plugin.name, async (output) => {
|
||||
if (output.type === "action" && output.action === "CHAT") {
|
||||
if (this.host.connector.connected) {
|
||||
this.sendChat(output.message);
|
||||
if (host.connector.connected) {
|
||||
sendChat(output.message);
|
||||
} else {
|
||||
this.messageQueue.push(output.message);
|
||||
messageQueue.push(output.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// TODO implement info command in lua?
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
import * as mock from "../../../test/mock.js";
|
||||
import * as lines from "../../../test/lib/factorio/lines.js";
|
||||
@@ -31,40 +32,44 @@ describe("global_chat plugin", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("class InstancePlugin", function() {
|
||||
let instancePlugin;
|
||||
describe("instance entrypoint", function() {
|
||||
let mockInstance;
|
||||
|
||||
before(async function() {
|
||||
instancePlugin = new instance.InstancePlugin(info, new mock.MockInstance(), new mock.MockHost());
|
||||
await instancePlugin.init();
|
||||
try {
|
||||
lib.registerPluginMessages([info]);
|
||||
} catch (err) {
|
||||
// Already registered by the full test suite
|
||||
}
|
||||
({ instance: mockInstance } = await mock.loadInstancePlugin(instance.default, info));
|
||||
});
|
||||
|
||||
describe(".handleChatEvent()", function() {
|
||||
describe("ChatEvent handler", function() {
|
||||
it("should send received chat as command", async function() {
|
||||
instancePlugin.instance.server.rconCommands = [];
|
||||
await instancePlugin.handleChatEvent(new ChatEvent("test", "User: message"));
|
||||
mockInstance.server.rconCommands = [];
|
||||
await mock.getHandler(mockInstance, ChatEvent)(new ChatEvent("test", "User: message"));
|
||||
assert.deepEqual(
|
||||
instancePlugin.instance.server.rconCommands,
|
||||
mockInstance.server.rconCommands,
|
||||
["/sc game.print('[test] User: message')"],
|
||||
);
|
||||
});
|
||||
it("should filter server-specific tags from received chat", async function() {
|
||||
instancePlugin.instance.server.rconCommands = [];
|
||||
await instancePlugin.handleChatEvent(new ChatEvent("test", "Train [train=1235]"));
|
||||
mockInstance.server.rconCommands = [];
|
||||
await mock.getHandler(mockInstance, ChatEvent)(new ChatEvent("test", "Train [train=1235]"));
|
||||
assert.deepEqual(
|
||||
instancePlugin.instance.server.rconCommands,
|
||||
mockInstance.server.rconCommands,
|
||||
["/sc game.print('[test] Train ')"],
|
||||
);
|
||||
});
|
||||
});
|
||||
describe(".onOutput()", function() {
|
||||
describe("output hook", function() {
|
||||
it("should forward chat", async function() {
|
||||
let count = 0;
|
||||
for (let [_, output] of lines.testLines) {
|
||||
for (let [line, output] of lines.testLines) {
|
||||
if (output.type === "action" && output.action === "CHAT") {
|
||||
instancePlugin.instance.connector.sentMessages = [];
|
||||
await instancePlugin.onOutput(output);
|
||||
assert(instancePlugin.instance.connector.sentMessages.length, "message was not sent");
|
||||
mockInstance.connector.sentMessages = [];
|
||||
await mockInstance.hooks.output.invoke(output, line);
|
||||
assert(mockInstance.connector.sentMessages.length, "message was not sent");
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
@@ -72,11 +77,11 @@ describe("global_chat plugin", function() {
|
||||
});
|
||||
it("should ignore regular output", async function() {
|
||||
let count = 0;
|
||||
for (let [_, output] of lines.testLines) {
|
||||
for (let [line, output] of lines.testLines) {
|
||||
if (output.type !== "action" || output.action !== "CHAT") {
|
||||
instancePlugin.instance.connector.sentMessages = [];
|
||||
await instancePlugin.onOutput(output);
|
||||
assert(!instancePlugin.instance.connector.sentMessages.length, "message was sent");
|
||||
mockInstance.connector.sentMessages = [];
|
||||
await mockInstance.hooks.output.invoke(output, line);
|
||||
assert(!mockInstance.connector.sentMessages.length, "message was sent");
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "path";
|
||||
import { BaseControllerPlugin } from "@clusterio/controller";
|
||||
import type { ControllerPluginContext } from "@clusterio/controller";
|
||||
import { Static } from "@sinclair/typebox";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
@@ -49,61 +49,39 @@ async function saveTechnologies(
|
||||
await lib.safeOutputFile(filePath, JSON.stringify([...technologies.entries()], null, "\t"));
|
||||
}
|
||||
|
||||
export class ControllerPlugin extends BaseControllerPlugin {
|
||||
technologies!: Map<string, Technology>;
|
||||
technologiesDirty = true;
|
||||
progressRateLimiter!: lib.RateLimiter;
|
||||
progressBroadcastId!: ReturnType<typeof setInterval> | null;
|
||||
progressToBroadcast!: Set<string>;
|
||||
export default async function(context: ControllerPluginContext) {
|
||||
const { controller, logger, plugin } = context;
|
||||
const technologies = await loadTechnologies(controller.config, logger);
|
||||
let technologiesDirty = true;
|
||||
const progressToBroadcast = new Set<string>();
|
||||
|
||||
async init() {
|
||||
this.technologies = await loadTechnologies(this.controller.config, this.logger);
|
||||
this.progressRateLimiter = new RateLimiter({
|
||||
maxRate: 1,
|
||||
action: () => this.broadcastProgress(),
|
||||
});
|
||||
|
||||
this.progressBroadcastId = null;
|
||||
this.progressToBroadcast = new Set();
|
||||
|
||||
this.controller.handle(ContributionEvent, this.handleContributionEvent.bind(this));
|
||||
this.controller.handle(FinishedEvent, this.handleFinishedEvent.bind(this));
|
||||
this.controller.handle(SyncTechnologiesRequest, this.handleSyncTechnologiesRequest.bind(this));
|
||||
}
|
||||
|
||||
async onSaveData() {
|
||||
if (this.technologiesDirty) {
|
||||
this.technologiesDirty = false;
|
||||
await saveTechnologies(this.controller.config, this.technologies, this.logger);
|
||||
}
|
||||
}
|
||||
|
||||
async onShutdown() {
|
||||
this.progressRateLimiter.cancel();
|
||||
}
|
||||
|
||||
broadcastProgress() {
|
||||
function broadcastProgress() {
|
||||
let techs = [];
|
||||
for (let name of this.progressToBroadcast) {
|
||||
let tech = this.technologies.get(name);
|
||||
for (let name of progressToBroadcast) {
|
||||
let tech = technologies.get(name);
|
||||
if (tech && tech.progress) {
|
||||
techs.push(new TechnologyProgress(name, tech.level, tech.progress));
|
||||
}
|
||||
}
|
||||
this.progressToBroadcast.clear();
|
||||
progressToBroadcast.clear();
|
||||
|
||||
if (techs.length) {
|
||||
this.controller.sendTo("allInstances", new ProgressEvent(techs));
|
||||
controller.sendTo("allInstances", new ProgressEvent(techs));
|
||||
}
|
||||
}
|
||||
|
||||
async handleContributionEvent(event: ContributionEvent) {
|
||||
const progressRateLimiter = new RateLimiter({
|
||||
maxRate: 1,
|
||||
action: broadcastProgress,
|
||||
});
|
||||
|
||||
controller.handle(ContributionEvent, async (event: ContributionEvent) => {
|
||||
let { name, level, contribution } = event;
|
||||
let tech = this.technologies.get(name);
|
||||
let tech = technologies.get(name);
|
||||
if (!tech) {
|
||||
tech = { level, progress: 0, researched: false };
|
||||
this.technologies.set(name, tech);
|
||||
this.technologiesDirty = true;
|
||||
technologies.set(name, tech);
|
||||
technologiesDirty = true;
|
||||
|
||||
// Ignore contribution to already researched technologies
|
||||
} else if (tech.level > level || tech.level === level && tech.researched) {
|
||||
@@ -124,31 +102,31 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
let newProgress = tech.progress! + contribution;
|
||||
if (newProgress < 1) {
|
||||
tech.progress = newProgress;
|
||||
this.progressToBroadcast.add(name);
|
||||
this.progressRateLimiter!.activate();
|
||||
progressToBroadcast.add(name);
|
||||
progressRateLimiter.activate();
|
||||
|
||||
} else {
|
||||
tech.researched = true;
|
||||
tech.progress = null;
|
||||
this.progressToBroadcast.delete(name);
|
||||
progressToBroadcast.delete(name);
|
||||
|
||||
this.controller.sendTo("allInstances", new FinishedEvent(name, tech.level));
|
||||
controller.sendTo("allInstances", new FinishedEvent(name, tech.level));
|
||||
}
|
||||
this.technologiesDirty = true;
|
||||
}
|
||||
technologiesDirty = true;
|
||||
});
|
||||
|
||||
async handleFinishedEvent(event: FinishedEvent) {
|
||||
controller.handle(FinishedEvent, async (event: FinishedEvent) => {
|
||||
let { name, level } = event;
|
||||
let tech = this.technologies.get(name);
|
||||
let tech = technologies.get(name);
|
||||
if (!tech || tech.level <= level) {
|
||||
this.controller.sendTo("allInstances", event);
|
||||
this.progressToBroadcast.delete(name);
|
||||
this.technologies.set(name, { level, progress: null, researched: true });
|
||||
this.technologiesDirty = true;
|
||||
controller.sendTo("allInstances", event);
|
||||
progressToBroadcast.delete(name);
|
||||
technologies.set(name, { level, progress: null, researched: true });
|
||||
technologiesDirty = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async handleSyncTechnologiesRequest(request: SyncTechnologiesRequest): Promise<TechnologySync[]> {
|
||||
controller.handle(SyncTechnologiesRequest, async (request: SyncTechnologiesRequest): Promise<TechnologySync[]> => {
|
||||
function baseLevel(name: string): number {
|
||||
let match = /-(\d+)$/.exec(name);
|
||||
if (!match) {
|
||||
@@ -159,14 +137,14 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
|
||||
for (let instanceTech of request.technologies) {
|
||||
let { name, level, progress, researched } = instanceTech;
|
||||
let tech = this.technologies.get(name);
|
||||
let tech = technologies.get(name);
|
||||
if (!tech) {
|
||||
this.technologies.set(name, { level, progress, researched });
|
||||
this.technologiesDirty = true;
|
||||
technologies.set(name, { level, progress, researched });
|
||||
technologiesDirty = true;
|
||||
if (progress) {
|
||||
this.progressToBroadcast.add(name);
|
||||
progressToBroadcast.add(name);
|
||||
} else if (researched || baseLevel(name) !== level) {
|
||||
this.controller.sendTo("allInstances", new FinishedEvent(name, level));
|
||||
controller.sendTo("allInstances", new FinishedEvent(name, level));
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -177,33 +155,44 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
if (tech.level < level || researched) {
|
||||
// Send update if the unlocked level is greater
|
||||
if (level - Number(!researched) > tech.level - Number(!tech.researched)) {
|
||||
this.controller.sendTo("allInstances", new FinishedEvent(name, level - Number(!researched)));
|
||||
controller.sendTo("allInstances", new FinishedEvent(name, level - Number(!researched)));
|
||||
}
|
||||
tech.level = level;
|
||||
tech.progress = progress;
|
||||
tech.researched = researched;
|
||||
|
||||
if (progress) {
|
||||
this.progressToBroadcast.add(name);
|
||||
progressToBroadcast.add(name);
|
||||
} else {
|
||||
this.progressToBroadcast.delete(name);
|
||||
progressToBroadcast.delete(name);
|
||||
}
|
||||
this.technologiesDirty = true;
|
||||
technologiesDirty = true;
|
||||
|
||||
} else if (tech.progress && progress && tech.progress < progress) {
|
||||
tech.progress = progress;
|
||||
this.progressToBroadcast.add(name);
|
||||
this.technologiesDirty = true;
|
||||
progressToBroadcast.add(name);
|
||||
technologiesDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.progressRateLimiter.activate();
|
||||
progressRateLimiter.activate();
|
||||
|
||||
let technologies = [];
|
||||
for (let [name, tech] of this.technologies) {
|
||||
technologies.push(new TechnologySync(name, tech.level, tech.progress, tech.researched));
|
||||
let result = [];
|
||||
for (let [name, tech] of technologies) {
|
||||
result.push(new TechnologySync(name, tech.level, tech.progress, tech.researched));
|
||||
}
|
||||
|
||||
return technologies;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
controller.hooks.save.attach(plugin.name, async () => {
|
||||
if (technologiesDirty) {
|
||||
technologiesDirty = false;
|
||||
await saveTechnologies(controller.config, technologies, logger);
|
||||
}
|
||||
});
|
||||
|
||||
controller.hooks.shutdown.attach(plugin.name, async () => {
|
||||
progressRateLimiter.cancel();
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
import type { InstancePluginContext } from "@clusterio/host";
|
||||
import {
|
||||
ContributionEvent,
|
||||
ProgressEvent,
|
||||
@@ -19,54 +19,57 @@ type IpcFinished = {
|
||||
level: number,
|
||||
};
|
||||
|
||||
export class InstancePlugin extends BaseInstancePlugin {
|
||||
syncStarted!: boolean;
|
||||
export default async function(context: InstancePluginContext) {
|
||||
const { instance, logger, plugin } = context;
|
||||
let syncStarted = false;
|
||||
|
||||
unexpectedError(err: Error) {
|
||||
this.logger.error(`Unexpected error:\n${err.stack}`);
|
||||
// Commands are sent one at a time so they execute in order
|
||||
let rconQueue: Promise<unknown> = Promise.resolve();
|
||||
function sendOrderedRcon(message: string, expectEmpty = false) {
|
||||
const result = rconQueue.then(() => instance.sendRcon(message, expectEmpty, plugin.name));
|
||||
rconQueue = result.catch(() => {});
|
||||
return result;
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.instance.server.on("ipc-research_sync:contribution", (tech: IpcContribution) => {
|
||||
this.researchContribution(tech).catch(err => this.unexpectedError(err));
|
||||
});
|
||||
this.instance.server.on("ipc-research_sync:finished", (tech: IpcFinished) => {
|
||||
this.researchFinished(tech).catch(err => this.unexpectedError(err));
|
||||
});
|
||||
|
||||
this.syncStarted = false;
|
||||
this.instance.handle(ProgressEvent, this.handleProgressEvent.bind(this));
|
||||
this.instance.handle(FinishedEvent, this.handleFinishedEvent.bind(this));
|
||||
function unexpectedError(err: Error) {
|
||||
logger.error(`Unexpected error:\n${err.stack}`);
|
||||
}
|
||||
|
||||
async researchContribution(tech: IpcContribution) {
|
||||
this.instance.sendTo("controller", new ContributionEvent(tech.name, tech.level, tech.contribution));
|
||||
async function researchContribution(tech: IpcContribution) {
|
||||
instance.sendTo("controller", new ContributionEvent(tech.name, tech.level, tech.contribution));
|
||||
}
|
||||
|
||||
async handleProgressEvent(event: ProgressEvent) {
|
||||
if (!this.syncStarted || !["starting", "running"].includes(this.instance.status)) {
|
||||
async function researchFinished(tech: IpcFinished) {
|
||||
instance.sendTo("controller", new FinishedEvent(tech.name, tech.level));
|
||||
}
|
||||
|
||||
instance.server.on("ipc-research_sync:contribution", (tech: IpcContribution) => {
|
||||
researchContribution(tech).catch(err => unexpectedError(err));
|
||||
});
|
||||
instance.server.on("ipc-research_sync:finished", (tech: IpcFinished) => {
|
||||
researchFinished(tech).catch(err => unexpectedError(err));
|
||||
});
|
||||
|
||||
instance.handle(ProgressEvent, async (event: ProgressEvent) => {
|
||||
if (!syncStarted || !["starting", "running"].includes(instance.status)) {
|
||||
return;
|
||||
}
|
||||
let techsJson = lib.escapeString(JSON.stringify(event.technologies));
|
||||
await this.sendOrderedRcon(`/sc research_sync.update_progress("${techsJson}")`, true);
|
||||
}
|
||||
await sendOrderedRcon(`/sc research_sync.update_progress("${techsJson}")`, true);
|
||||
});
|
||||
|
||||
async researchFinished(tech: IpcFinished) {
|
||||
this.instance.sendTo("controller", new FinishedEvent(tech.name, tech.level));
|
||||
}
|
||||
|
||||
async handleFinishedEvent(event: FinishedEvent) {
|
||||
if (!this.syncStarted || !["starting", "running"].includes(this.instance.status)) {
|
||||
instance.handle(FinishedEvent, async (event: FinishedEvent) => {
|
||||
if (!syncStarted || !["starting", "running"].includes(instance.status)) {
|
||||
return;
|
||||
}
|
||||
let { name, level } = event;
|
||||
await this.sendOrderedRcon(
|
||||
await sendOrderedRcon(
|
||||
`/sc research_sync.research_technology("${lib.escapeString(name)}", ${level})`, true
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
async onStart() {
|
||||
let dumpJson = await this.sendOrderedRcon("/sc research_sync.dump_technologies()");
|
||||
instance.hooks.start.attach(plugin.name, async () => {
|
||||
let dumpJson = await sendOrderedRcon("/sc research_sync.dump_technologies()");
|
||||
let techsToSend = [];
|
||||
let instanceTechs = new Map();
|
||||
for (let tech of JSON.parse(dumpJson)) {
|
||||
@@ -79,8 +82,8 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
instanceTechs.set(tech.name, tech);
|
||||
}
|
||||
|
||||
let controllerTechs = await this.instance.sendTo("controller", new SyncTechnologiesRequest(techsToSend));
|
||||
this.syncStarted = true;
|
||||
let controllerTechs = await instance.sendTo("controller", new SyncTechnologiesRequest(techsToSend));
|
||||
syncStarted = true;
|
||||
let techsToSync = [];
|
||||
for (let controllerTech of controllerTechs) {
|
||||
let { name, level, progress, researched } = controllerTech;
|
||||
@@ -97,7 +100,7 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
|
||||
if (techsToSync.length) {
|
||||
let syncJson = lib.escapeString(JSON.stringify(techsToSync));
|
||||
await this.sendOrderedRcon(`/sc research_sync.sync_technologies("${syncJson}")`, true);
|
||||
await sendOrderedRcon(`/sc research_sync.sync_technologies("${syncJson}")`, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
import type { Instance, InstancePluginContext } from "@clusterio/host";
|
||||
|
||||
import { Gauge } from "@clusterio/lib";
|
||||
|
||||
@@ -103,79 +103,81 @@ function setForceFlowStatistic(
|
||||
}
|
||||
|
||||
|
||||
export class InstancePlugin extends BaseInstancePlugin {
|
||||
async gatherMetrics() {
|
||||
let string = await this.sendRcon("/sc statistics_exporter.export()");
|
||||
let stats: IpcStats;
|
||||
try {
|
||||
stats = JSON.parse(string);
|
||||
} catch (err: any) {
|
||||
throw new Error(`Error parsing statistics JSON: ${err.message}, content "${string}"`);
|
||||
}
|
||||
export async function gatherMetrics(instance: Instance, pluginName: string) {
|
||||
let string = await instance.sendRcon("/sc statistics_exporter.export()", false, pluginName);
|
||||
let stats: IpcStats;
|
||||
try {
|
||||
stats = JSON.parse(string);
|
||||
} catch (err: any) {
|
||||
throw new Error(`Error parsing statistics JSON: ${err.message}, content "${string}"`);
|
||||
}
|
||||
|
||||
let instanceId = this.instance.id;
|
||||
instanceGameTicksTotal.labels(String(instanceId)).set(stats.game_tick);
|
||||
instancePlayerCount.labels(String(instanceId)).set(stats.player_count);
|
||||
let instanceId = instance.id;
|
||||
instanceGameTicksTotal.labels(String(instanceId)).set(stats.game_tick);
|
||||
instancePlayerCount.labels(String(instanceId)).set(stats.player_count);
|
||||
|
||||
// Remove existing platform mappings and stats for this instance
|
||||
instancePlatformMapping.removeAll({ instance_id: String(instanceId) });
|
||||
instancePlatformSpeed.removeAll({ instance_id: String(instanceId) });
|
||||
instancePlatformWeight.removeAll({ instance_id: String(instanceId) });
|
||||
// Remove existing platform mappings and stats for this instance
|
||||
instancePlatformMapping.removeAll({ instance_id: String(instanceId) });
|
||||
instancePlatformSpeed.removeAll({ instance_id: String(instanceId) });
|
||||
instancePlatformWeight.removeAll({ instance_id: String(instanceId) });
|
||||
|
||||
// Set new platform mappings and stats
|
||||
for (let [platformName, platform] of Object.entries(stats.platforms)) {
|
||||
instancePlatformMapping.labels(
|
||||
String(instanceId),
|
||||
platformName,
|
||||
platform.force,
|
||||
platform.surface
|
||||
).set(1);
|
||||
// Set new platform mappings and stats
|
||||
for (let [platformName, platform] of Object.entries(stats.platforms)) {
|
||||
instancePlatformMapping.labels(
|
||||
String(instanceId),
|
||||
platformName,
|
||||
platform.force,
|
||||
platform.surface
|
||||
).set(1);
|
||||
|
||||
instancePlatformSpeed.labels(
|
||||
String(instanceId),
|
||||
platform.force,
|
||||
platform.surface
|
||||
).set(platform.speed);
|
||||
instancePlatformSpeed.labels(
|
||||
String(instanceId),
|
||||
platform.force,
|
||||
platform.surface
|
||||
).set(platform.speed);
|
||||
|
||||
instancePlatformWeight.labels(
|
||||
String(instanceId),
|
||||
platform.force,
|
||||
platform.surface
|
||||
).set(platform.weight);
|
||||
}
|
||||
instancePlatformWeight.labels(
|
||||
String(instanceId),
|
||||
platform.force,
|
||||
platform.surface
|
||||
).set(platform.weight);
|
||||
}
|
||||
|
||||
for (let [surfaceName, surfaceStats] of Object.entries(stats.surface_statistics)) {
|
||||
for (let [forceName, flowStatistics] of Object.entries(surfaceStats.force_flow_statistics)) {
|
||||
for (let [statisticName, statistic] of Object.entries(flowStatistics)) {
|
||||
for (let [direction, counts] of Object.entries(statistic)) {
|
||||
// eslint-disable-next-line max-depth
|
||||
for (let [item, value] of Object.entries(counts)) {
|
||||
setForceFlowStatistic(
|
||||
instanceId,
|
||||
surfaceName,
|
||||
forceName,
|
||||
statisticName,
|
||||
direction,
|
||||
item,
|
||||
value
|
||||
);
|
||||
}
|
||||
for (let [surfaceName, surfaceStats] of Object.entries(stats.surface_statistics)) {
|
||||
for (let [forceName, flowStatistics] of Object.entries(surfaceStats.force_flow_statistics)) {
|
||||
for (let [statisticName, statistic] of Object.entries(flowStatistics)) {
|
||||
for (let [direction, counts] of Object.entries(statistic)) {
|
||||
// eslint-disable-next-line max-depth
|
||||
for (let [item, value] of Object.entries(counts)) {
|
||||
setForceFlowStatistic(
|
||||
instanceId,
|
||||
surfaceName,
|
||||
forceName,
|
||||
statisticName,
|
||||
direction,
|
||||
item,
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let [direction, counts] of Object.entries(surfaceStats.game_flow_statistics.pollution_statistics)) {
|
||||
for (let [item, value] of Object.entries(counts)) {
|
||||
instanceGameFlowStatistics.labels(
|
||||
String(instanceId), surfaceName, "pollution_statistics", direction, item
|
||||
).set(value);
|
||||
}
|
||||
for (let [direction, counts] of Object.entries(surfaceStats.game_flow_statistics.pollution_statistics)) {
|
||||
for (let [item, value] of Object.entries(counts)) {
|
||||
instanceGameFlowStatistics.labels(
|
||||
String(instanceId), surfaceName, "pollution_statistics", direction, item
|
||||
).set(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onMetrics() {
|
||||
if (this.instance.status !== "running") {
|
||||
export default async function(context: InstancePluginContext) {
|
||||
const { instance, plugin } = context;
|
||||
|
||||
instance.hooks.metrics.attach(plugin.name, async () => {
|
||||
if (instance.status !== "running") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -183,9 +185,9 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
// take a long time for the command to go through if the command
|
||||
// stream is overloaded. Should the timeout be exceeded the
|
||||
// previous values for the metrics will end up being sent to controller.
|
||||
let timeoutMs = this.instance.config.get("statistics_exporter.command_timeout") * 1000;
|
||||
await lib.timeout(this.gatherMetrics(), timeoutMs, undefined);
|
||||
}
|
||||
let timeoutMs = instance.config.get("statistics_exporter.command_timeout") * 1000;
|
||||
await lib.timeout(gatherMetrics(instance, plugin.name), timeoutMs, undefined);
|
||||
});
|
||||
}
|
||||
|
||||
export const _instancePlayerCount = instancePlayerCount;
|
||||
|
||||
@@ -6,25 +6,23 @@ import { plugin as info } from "../dist/node/index.js";
|
||||
|
||||
|
||||
describe("statistics_exporter plugin", function() {
|
||||
describe("class InstancePlugin", function() {
|
||||
let instancePlugin;
|
||||
let mockInstance = new mock.MockInstance();
|
||||
mockInstance.mockConfigEntries.set("statistics_exporter.command_timeout", 1);
|
||||
describe("instance entrypoint", function() {
|
||||
let mockInstance;
|
||||
before(async function() {
|
||||
instancePlugin = new instance.InstancePlugin(info, mockInstance);
|
||||
await instancePlugin.init();
|
||||
({ instance: mockInstance } = await mock.loadInstancePlugin(instance.default, info));
|
||||
mockInstance.mockConfigEntries.set("statistics_exporter.command_timeout", 1);
|
||||
});
|
||||
|
||||
describe(".onMetrics()", function() {
|
||||
describe("metrics hook", function() {
|
||||
it("should not send commands before running", async function() {
|
||||
instancePlugin.instance.server.rconCommands.length = 0;
|
||||
instancePlugin.instance.status = "starting";
|
||||
await instancePlugin.onMetrics();
|
||||
assert(instancePlugin.instance.server.rconCommands.length === 0, "commands were run");
|
||||
mockInstance.server.rconCommands.length = 0;
|
||||
mockInstance.status = "starting";
|
||||
await mockInstance.hooks.metrics.invoke();
|
||||
assert(mockInstance.server.rconCommands.length === 0, "commands were run");
|
||||
});
|
||||
it("should record statistics", async function() {
|
||||
instancePlugin.instance.status = "running";
|
||||
instancePlugin.instance.server.rconCommandResults.set(
|
||||
mockInstance.status = "running";
|
||||
mockInstance.server.rconCommandResults.set(
|
||||
"/sc statistics_exporter.export()",
|
||||
JSON.stringify({
|
||||
game_tick: 100,
|
||||
@@ -58,7 +56,7 @@ describe("statistics_exporter plugin", function() {
|
||||
})
|
||||
);
|
||||
|
||||
await instancePlugin.onMetrics();
|
||||
await mockInstance.hooks.metrics.invoke();
|
||||
|
||||
assert.equal(instance._instancePlayerCount.labels("7357").get(), 3);
|
||||
assert.equal(instance._instanceGameTicksTotal.labels("7357").get(), 100);
|
||||
@@ -90,7 +88,7 @@ describe("statistics_exporter plugin", function() {
|
||||
});
|
||||
it("should pass on JSON parse errors", async function() {
|
||||
let string = "An error occured\n";
|
||||
instancePlugin.instance.server.rconCommandResults.set(
|
||||
mockInstance.server.rconCommandResults.set(
|
||||
"/sc statistics_exporter.export()", string
|
||||
);
|
||||
|
||||
@@ -102,7 +100,7 @@ describe("statistics_exporter plugin", function() {
|
||||
}
|
||||
|
||||
await assert.rejects(
|
||||
instancePlugin.onMetrics(),
|
||||
instance.gatherMetrics(mockInstance, info.name),
|
||||
new Error(`Error parsing statistics JSON: ${errorMessage}, content "${string}"`)
|
||||
);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user