mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 07:24:52 +09:00
Merge pull request #498 from Hornwitser/typescript-host
Port host package to TypeScript
This commit is contained in:
26 files changed
+832
-680
No files matched your search
@@ -21,7 +21,7 @@ export default class ControllerRouter {
|
||||
*/
|
||||
forwardMessage(
|
||||
origin: ControlConnection | HostConnection,
|
||||
message: lib.MessageSrcDst,
|
||||
message: lib.MessageRoutable,
|
||||
hasFallback: boolean
|
||||
): boolean {
|
||||
if (!["request", "response", "responseError", "event"].includes(message.type)) {
|
||||
@@ -95,7 +95,7 @@ export default class ControllerRouter {
|
||||
|
||||
broadcastMessage(
|
||||
origin: lib.Link,
|
||||
message: lib.MessageSrcDst,
|
||||
message: lib.MessageRoutable,
|
||||
) {
|
||||
let dst = message.dst;
|
||||
if (message.type !== "event") {
|
||||
@@ -118,7 +118,7 @@ export default class ControllerRouter {
|
||||
}
|
||||
|
||||
warnUnrouted(
|
||||
message: lib.MessageSrcDst,
|
||||
message: lib.MessageRoutable,
|
||||
msg: string
|
||||
) {
|
||||
let dst = message.dst;
|
||||
|
||||
@@ -84,7 +84,7 @@ export default class HostConnection extends BaseConnection {
|
||||
this.handle(lib.InstancePlayerUpdateEvent, this.handleInstancePlayerUpdateEvent.bind(this));
|
||||
}
|
||||
|
||||
validateIngress(message: lib.MessageSrcDst) {
|
||||
validateIngress(message: lib.MessageRoutable) {
|
||||
let origin = this.connector.dst;
|
||||
switch (message.src.type) {
|
||||
case lib.Address.control:
|
||||
|
||||
@@ -12,36 +12,37 @@
|
||||
* @example
|
||||
* npx clusteriohost run
|
||||
*/
|
||||
"use strict";
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const yargs = require("yargs");
|
||||
const setBlocking = require("set-blocking");
|
||||
const version = require("./package").version;
|
||||
const winston = require("winston");
|
||||
require("winston-daily-rotate-file");
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import yargs from "yargs";
|
||||
import setBlocking from "set-blocking";
|
||||
import { version } from "./package.json";
|
||||
import winston from "winston";
|
||||
import "winston-daily-rotate-file";
|
||||
|
||||
// internal libraries
|
||||
const lib = require("@clusterio/lib");
|
||||
const { ConsoleTransport, levels, logger } = lib;
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { ConsoleTransport, levels, logger } from "@clusterio/lib";
|
||||
|
||||
const Host = require("./src/Host");
|
||||
import Host from "./src/Host";
|
||||
|
||||
|
||||
class HostConnector extends lib.WebSocketClientConnector {
|
||||
constructor(hostConfig, tlsCa, pluginInfos) {
|
||||
export class HostConnector extends lib.WebSocketClientConnector {
|
||||
constructor(
|
||||
public hostConfig: lib.HostConfig,
|
||||
tlsCa: string | undefined,
|
||||
public pluginInfos: lib.PluginInfo[]
|
||||
) {
|
||||
super(
|
||||
hostConfig.get("host.controller_url"),
|
||||
hostConfig.get("host.max_reconnect_delay"),
|
||||
tlsCa
|
||||
);
|
||||
this.hostConfig = hostConfig;
|
||||
this.pluginInfos = pluginInfos;
|
||||
}
|
||||
|
||||
register() {
|
||||
logger.info("Connector | registering host");
|
||||
let plugins = {};
|
||||
let plugins: Record<string, string> = {};
|
||||
for (let pluginInfo of this.pluginInfos) {
|
||||
plugins[pluginInfo.name] = pluginInfo.version;
|
||||
}
|
||||
@@ -98,7 +99,7 @@ async function startHost() {
|
||||
.command("run", "Run host")
|
||||
.demandCommand(1, "You need to specify a command to run")
|
||||
.strict()
|
||||
.argv
|
||||
.parseSync()
|
||||
;
|
||||
|
||||
logger.add(new winston.transports.DailyRotateFile({
|
||||
@@ -111,10 +112,10 @@ async function startHost() {
|
||||
logger.add(new ConsoleTransport({
|
||||
level: args.logLevel,
|
||||
format: new lib.TerminalFormat(),
|
||||
filter: info => info.instance_id === undefined,
|
||||
filter: (info: any) => info.instance_id === undefined,
|
||||
}));
|
||||
}
|
||||
lib.handleUnhandledErrors(logger);
|
||||
lib.handleUnhandledErrors();
|
||||
|
||||
let command = args._[0];
|
||||
if (command === "run") {
|
||||
@@ -124,8 +125,8 @@ async function startHost() {
|
||||
logger.info(`Loading available plugins from ${args.pluginList}`);
|
||||
let pluginList = new Map();
|
||||
try {
|
||||
pluginList = new Map(JSON.parse(await fs.readFile(args.pluginList)));
|
||||
} catch (err) {
|
||||
pluginList = new Map(JSON.parse(await fs.readFile(args.pluginList, "utf8")));
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
@@ -146,9 +147,9 @@ async function startHost() {
|
||||
logger.info(`Loading config from ${args.config}`);
|
||||
let hostConfig = new lib.HostConfig("host");
|
||||
try {
|
||||
await hostConfig.load(JSON.parse(await fs.readFile(args.config)));
|
||||
await hostConfig.load(JSON.parse(await fs.readFile(args.config, "utf8")));
|
||||
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
logger.info("Config not found, initializing new config");
|
||||
await hostConfig.init();
|
||||
@@ -194,10 +195,10 @@ async function startHost() {
|
||||
return;
|
||||
}
|
||||
|
||||
let tlsCa = null;
|
||||
let tlsCa: string | undefined;
|
||||
let tlsCaPath = hostConfig.get("host.tls_ca");
|
||||
if (tlsCaPath) {
|
||||
tlsCa = await fs.readFile(tlsCaPath);
|
||||
tlsCa = await fs.readFile(tlsCaPath, "utf8");
|
||||
}
|
||||
|
||||
let hostConnector = new HostConnector(hostConfig, tlsCa, pluginInfos);
|
||||
@@ -4,12 +4,13 @@
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.13",
|
||||
"main": "host.js",
|
||||
"main": "dist/host.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"bin": {
|
||||
"clusteriohost": "./host.js"
|
||||
"clusteriohost": "./dist/host.js"
|
||||
},
|
||||
"keywords": [
|
||||
"factorio"
|
||||
@@ -33,5 +34,15 @@
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sinclair/typebox": "^0.30.4",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/pidusage": "^2.0.2",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@types/set-blocking": "^2.0.0",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,28 @@
|
||||
"use strict";
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const events = require("events");
|
||||
const pidusage = require("pidusage");
|
||||
const setBlocking = require("set-blocking");
|
||||
const phin = require("phin");
|
||||
const stream = require("stream");
|
||||
const util = require("util");
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import events from "events";
|
||||
import pidusage from "pidusage";
|
||||
import setBlocking from "set-blocking";
|
||||
import phin from "phin";
|
||||
import stream from "stream";
|
||||
import util from "util";
|
||||
|
||||
// internal libraries
|
||||
const lib = require("@clusterio/lib");
|
||||
const { logger } = lib;
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { logger } from "@clusterio/lib";
|
||||
|
||||
const Instance = require("./Instance");
|
||||
const InstanceConnection = require("./InstanceConnection");
|
||||
import type { HostConnector } from "../host";
|
||||
import Instance from "./Instance";
|
||||
import InstanceConnection from "./InstanceConnection";
|
||||
|
||||
const finished = util.promisify(stream.finished);
|
||||
|
||||
|
||||
function checkRequestSaveName(name) {
|
||||
function checkRequestSaveName(name: string) {
|
||||
try {
|
||||
lib.checkFilename(name);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
throw new lib.RequestError(`Save name ${err.message}`);
|
||||
}
|
||||
}
|
||||
@@ -33,22 +34,22 @@ function checkRequestSaveName(name) {
|
||||
* instance definitions and return a mapping of instance id to
|
||||
* instanceInfo objects.
|
||||
*
|
||||
* @param {string} instancesDir - Directory containing instances
|
||||
* @returns {Promise<Map<integer, Object>>}
|
||||
* @param instancesDir - Directory containing instances
|
||||
* @returns
|
||||
* mapping between instance id and information about this instance.
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
async function discoverInstances(instancesDir) {
|
||||
let instanceInfos = new Map();
|
||||
async function discoverInstances(instancesDir: string) {
|
||||
let instanceInfos = new Map<number, { path: string, config: lib.InstanceConfig }>();
|
||||
for (let entry of await fs.readdir(instancesDir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
let instanceConfig = new lib.InstanceConfig("host");
|
||||
let configPath = path.join(instancesDir, entry.name, "instance.json");
|
||||
|
||||
try {
|
||||
await instanceConfig.load(JSON.parse(await fs.readFile(configPath)));
|
||||
await instanceConfig.load(JSON.parse(await fs.readFile(configPath, "utf8")));
|
||||
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
continue; // Ignore folders without config.json
|
||||
}
|
||||
@@ -81,27 +82,24 @@ const instanceStartingMessages = new Set([
|
||||
]);
|
||||
|
||||
class HostRouter {
|
||||
/** @type {module:host/src/Host} */
|
||||
host;
|
||||
|
||||
constructor(host) {
|
||||
this.host = host;
|
||||
}
|
||||
constructor(
|
||||
public host: Host
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Forward a message to the next hop towards its destination.
|
||||
*
|
||||
* @param {module:lib.Link} origin -
|
||||
* @param origin -
|
||||
* Link the message originated from.
|
||||
* @param {module:lib.Message} message - Message to process.
|
||||
* @param {boolean} hasFallback - is fallback available
|
||||
* @returns {boolean} true if the message was handled, false if fallback
|
||||
* @param message - Message to process.
|
||||
* @param hasFallback - is fallback available
|
||||
* @returns true if the message was handled, false if fallback
|
||||
* is requested
|
||||
*/
|
||||
forwardMessage(origin, message, hasFallback) {
|
||||
forwardMessage(origin: lib.Link, message: lib.MessageRoutable, hasFallback: boolean) {
|
||||
let dst = message.dst;
|
||||
let nextHop;
|
||||
let msg;
|
||||
let nextHop: lib.Link | undefined;
|
||||
let msg: string | undefined;
|
||||
if (dst.type === lib.Address.broadcast) {
|
||||
this.broadcastMessage(origin, message);
|
||||
return true;
|
||||
@@ -113,16 +111,19 @@ class HostRouter {
|
||||
) {
|
||||
nextHop = this.host;
|
||||
} else if (dst.type === lib.Address.instance && this.host.instanceInfos.has(dst.id)) {
|
||||
nextHop = this.host.instanceConnections.get(dst.id);
|
||||
nextHop = this.host.instanceConnections.get(dst.id)!;
|
||||
}
|
||||
|
||||
if (nextHop === origin) {
|
||||
msg = `Message would return back to sender ${origin.dst}.`;
|
||||
msg = `Message would return back to sender ${origin.connector.dst}.`;
|
||||
nextHop = undefined;
|
||||
}
|
||||
|
||||
if (message.type === "request") {
|
||||
if (dst.type === lib.Address.instance && instanceStartingMessages.has(message.name)) {
|
||||
if (
|
||||
dst.type === lib.Address.instance
|
||||
&& instanceStartingMessages.has((message as lib.MessageRequest).name)
|
||||
) {
|
||||
this.wakeInstance(origin, message, nextHop);
|
||||
return true;
|
||||
|
||||
@@ -151,7 +152,7 @@ class HostRouter {
|
||||
return true;
|
||||
}
|
||||
|
||||
broadcastMessage(origin, message) {
|
||||
broadcastMessage(origin: lib.Link, message: lib.MessageRoutable) {
|
||||
let dst = message.dst;
|
||||
if (message.type !== "event") {
|
||||
this.warnUnrouted(message, `Unexpected broadcast of ${message.type}`);
|
||||
@@ -168,14 +169,14 @@ class HostRouter {
|
||||
if (this.host !== origin) {
|
||||
this.host.connector.send(message);
|
||||
} else {
|
||||
logger.warn(`Received control broadcast of ${message.name} from master.`);
|
||||
logger.warn(`Received control broadcast of ${(message as lib.MessageEvent).name} from master.`);
|
||||
}
|
||||
} else {
|
||||
this.warnUnrouted(message, `Unexpected broacdast target ${dst.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
wakeInstance(origin, message, nextHop) {
|
||||
wakeInstance(origin: lib.Link, message: lib.MessageRoutable, nextHop?: lib.Link) {
|
||||
let dst = message.dst;
|
||||
if (nextHop) {
|
||||
origin.connector.sendResponseError(
|
||||
@@ -200,24 +201,24 @@ class HostRouter {
|
||||
});
|
||||
}
|
||||
|
||||
sendMessage(nextHop, message, origin) {
|
||||
sendMessage(nextHop: lib.Link, message: lib.MessageRoutable, origin: lib.Link) {
|
||||
try {
|
||||
if (message.type === "request") {
|
||||
nextHop.forwardRequest(message, origin);
|
||||
nextHop.forwardRequest(message as lib.MessageRequest, origin);
|
||||
} else {
|
||||
nextHop.connector.send(message);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (message.type === "request") {
|
||||
origin.connector.sendResponseError(
|
||||
new lib.ResponseError(err.message, err.code, err.stack), message.src
|
||||
);
|
||||
}
|
||||
logger.warn(`Failed to deliver ${message.name || "message"} (${message.type}): ${err.message}`);
|
||||
logger.warn(`Failed to deliver ${(message as any).name || "message"} (${message.type}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
warnUnrouted(message, msg) {
|
||||
warnUnrouted(message: lib.MessageRoutable, msg?: string) {
|
||||
let dst = message.dst;
|
||||
let baseMsg = `No destination for ${message.constructor.name} routed from ${message.src} to ${dst}`;
|
||||
logger.warn(msg ? `${baseMsg}: ${msg}.` : `${baseMsg}.`);
|
||||
@@ -228,39 +229,50 @@ class HostRouter {
|
||||
* Handles running the host
|
||||
*
|
||||
* Connects to the controller over the WebSocket and manages intsances.
|
||||
* @alias module:host/src/Host
|
||||
*/
|
||||
class Host extends lib.Link {
|
||||
constructor(connector, hostConfig, tlsCa, pluginInfos) {
|
||||
export default class Host extends lib.Link {
|
||||
declare ["connector"]: HostConnector;
|
||||
|
||||
router = new HostRouter(this);
|
||||
/**
|
||||
* Certificate authority used to validate TLS connections to the controller.
|
||||
*/
|
||||
tlsCa?: string;
|
||||
pluginInfos: lib.PluginInfo[];
|
||||
config: lib.HostConfig;
|
||||
|
||||
instanceConnections = new Map<number, InstanceConnection>();
|
||||
discoveredInstanceInfos = new Map<number, { path: string, config: lib.InstanceConfig }>();
|
||||
instanceInfos = new Map<number, { path: string, config: lib.InstanceConfig }>();
|
||||
|
||||
adminlist = new Set<string>();
|
||||
banlist = new Map<string, string>();
|
||||
whitelist = new Set<string>();
|
||||
|
||||
serverVersion = "unknown";
|
||||
serverPlugins = new Map<string, string>();
|
||||
|
||||
_startup = true;
|
||||
_disconnecting = false;
|
||||
_shuttingDown = false;
|
||||
|
||||
constructor(
|
||||
connector: HostConnector,
|
||||
hostConfig: lib.HostConfig,
|
||||
tlsCa: string | undefined,
|
||||
pluginInfos: lib.PluginInfo[]
|
||||
) {
|
||||
super(connector);
|
||||
|
||||
this.router = new HostRouter(this);
|
||||
this.pluginInfos = pluginInfos;
|
||||
this.config = hostConfig;
|
||||
|
||||
/**
|
||||
* Certificate authority used to validate TLS connections to the controller.
|
||||
* @type {?string}
|
||||
*/
|
||||
this.tlsCa = tlsCa;
|
||||
|
||||
this.instanceConnections = new Map();
|
||||
this.discoveredInstanceInfos = new Map();
|
||||
this.instanceInfos = new Map();
|
||||
|
||||
this.adminlist = new Set();
|
||||
this.banlist = new Map();
|
||||
this.whitelist = new Set();
|
||||
this.pluginInfos = pluginInfos;
|
||||
this.config = hostConfig;
|
||||
|
||||
this.connector.on("hello", data => {
|
||||
this.serverVersion = data.version;
|
||||
this.serverPlugins = new Map(Object.entries(data.plugins));
|
||||
});
|
||||
|
||||
this._startup = true;
|
||||
this._disconnecting = false;
|
||||
this._shuttingDown = false;
|
||||
|
||||
this.connector.on("connect", () => {
|
||||
if (this._shuttingDown) {
|
||||
return;
|
||||
@@ -320,11 +332,11 @@ class Host extends lib.Link {
|
||||
this.handle(lib.InstanceDeleteInternalRequest, this.handleInstanceDeleteInternalRequest.bind(this));
|
||||
}
|
||||
|
||||
async _createNewInstanceDir(name) {
|
||||
async _createNewInstanceDir(name: string) {
|
||||
name = lib.cleanFilename(name);
|
||||
try {
|
||||
lib.checkFilename(name);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
throw new Error(`Instance folder was unepectedly invalid: name ${err.message}`);
|
||||
}
|
||||
|
||||
@@ -333,7 +345,7 @@ class Host extends lib.Link {
|
||||
let candidateDir = path.join(instancesDir, await lib.findUnusedName(instancesDir, name));
|
||||
try {
|
||||
await fs.mkdir(candidateDir);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "EEXIST") {
|
||||
continue;
|
||||
}
|
||||
@@ -344,15 +356,17 @@ class Host extends lib.Link {
|
||||
throw Error("Unable to create instance dir, retry threshold reached");
|
||||
}
|
||||
|
||||
async broadcastEventToInstance(event) {
|
||||
async broadcastEventToInstance<T>(event: lib.Event<T>) {
|
||||
for (let instanceConnection of this.instanceConnections.values()) {
|
||||
if (event.constructor.plugin && !instanceConnection.plugins.has(event.plugin)) { continue; }
|
||||
if (event.constructor.plugin && !instanceConnection.plugins.has(event.constructor.plugin)) {
|
||||
continue;
|
||||
}
|
||||
instanceConnection.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
async handleSyncUserListsEvent(request) {
|
||||
let updateList = (list, updatedList, Event) => {
|
||||
async handleSyncUserListsEvent(event: lib.SyncUserListsEvent) {
|
||||
let updateList = (list: Set<string>, updatedList: Set<string>, Event: lib.EventClass<unknown>) => {
|
||||
let added = new Set(updatedList);
|
||||
let removed = new Set(list);
|
||||
list.forEach(el => added.delete(el));
|
||||
@@ -369,10 +383,10 @@ class Host extends lib.Link {
|
||||
}
|
||||
};
|
||||
|
||||
updateList(this.adminlist, request.adminlist, lib.InstanceAdminlistUpdateEvent);
|
||||
updateList(this.whitelist, request.whitelist, lib.InstanceWhitelistUpdateEvent);
|
||||
updateList(this.adminlist, event.adminlist, lib.InstanceAdminlistUpdateEvent);
|
||||
updateList(this.whitelist, event.whitelist, lib.InstanceWhitelistUpdateEvent);
|
||||
|
||||
let addedOrChanged = new Map(request.banlist);
|
||||
let addedOrChanged = new Map(event.banlist);
|
||||
let removed = new Set(this.banlist.keys());
|
||||
addedOrChanged.forEach((_, name) => removed.delete(name));
|
||||
this.banlist.forEach((reason, name) => {
|
||||
@@ -392,8 +406,8 @@ class Host extends lib.Link {
|
||||
}
|
||||
}
|
||||
|
||||
async handleAdminlistUpdateEvent(request) {
|
||||
let { name, admin } = request;
|
||||
async handleAdminlistUpdateEvent(event: lib.InstanceAdminlistUpdateEvent) {
|
||||
let { name, admin } = event;
|
||||
if (admin) {
|
||||
this.adminlist.add(name);
|
||||
} else {
|
||||
@@ -401,8 +415,8 @@ class Host extends lib.Link {
|
||||
}
|
||||
}
|
||||
|
||||
async handleBanlistUpdateEvent(request) {
|
||||
let { name, banned, reason } = request;
|
||||
async handleBanlistUpdateEvent(event: lib.InstanceBanlistUpdateEvent) {
|
||||
let { name, banned, reason } = event;
|
||||
if (banned) {
|
||||
this.banlist.set(name, reason);
|
||||
} else {
|
||||
@@ -410,8 +424,8 @@ class Host extends lib.Link {
|
||||
}
|
||||
}
|
||||
|
||||
async handleWhitelistUpdateEvent(request) {
|
||||
let { name, whitelisted } = request;
|
||||
async handleWhitelistUpdateEvent(event: lib.InstanceWhitelistUpdateEvent) {
|
||||
let { name, whitelisted } = event;
|
||||
if (whitelisted) {
|
||||
this.whitelist.add(name);
|
||||
} else {
|
||||
@@ -419,21 +433,21 @@ class Host extends lib.Link {
|
||||
}
|
||||
}
|
||||
|
||||
async handleInstanceAssignInternalRequest(request) {
|
||||
async handleInstanceAssignInternalRequest(request: lib.InstanceAssignInternalRequest) {
|
||||
let { instanceId, config } = request;
|
||||
let instanceInfo = this.instanceInfos.get(instanceId);
|
||||
if (instanceInfo) {
|
||||
instanceInfo.config.update(config, true, "controller");
|
||||
instanceInfo.config.update(config as any, true, "controller");
|
||||
logger.verbose(`Updated config for ${instanceInfo.path}`, this.instanceLogMeta(instanceId, instanceInfo));
|
||||
|
||||
} else {
|
||||
instanceInfo = this.discoveredInstanceInfos.get(instanceId);
|
||||
if (instanceInfo) {
|
||||
instanceInfo.config.update(config, true, "controller");
|
||||
instanceInfo.config.update(config as any, true, "controller");
|
||||
|
||||
} else {
|
||||
let instanceConfig = new lib.InstanceConfig("host");
|
||||
await instanceConfig.load(config, "controller");
|
||||
await instanceConfig.load(config as any, "controller");
|
||||
|
||||
let instanceDir = await this._createNewInstanceDir(instanceConfig.get("instance.name"));
|
||||
|
||||
@@ -472,7 +486,7 @@ class Host extends lib.Link {
|
||||
);
|
||||
}
|
||||
|
||||
async handleInstanceUnassignInternalRequest(request) {
|
||||
async handleInstanceUnassignInternalRequest(request: lib.InstanceUnassignInternalRequest) {
|
||||
let instanceId = request.instanceId;
|
||||
let instanceInfo = this.instanceInfos.get(instanceId);
|
||||
if (instanceInfo) {
|
||||
@@ -486,7 +500,7 @@ class Host extends lib.Link {
|
||||
}
|
||||
}
|
||||
|
||||
instanceLogMeta(instanceId, instanceInfo) {
|
||||
instanceLogMeta(instanceId: number, instanceInfo?: { config: lib.InstanceConfig }) {
|
||||
instanceInfo = instanceInfo || this.instanceInfos.get(instanceId);
|
||||
if (!instanceInfo) {
|
||||
return { instance_id: instanceId, instance_name: String(instanceId) };
|
||||
@@ -494,7 +508,7 @@ class Host extends lib.Link {
|
||||
return { instance_id: instanceId, instance_name: instanceInfo.config.get("instance.name") };
|
||||
}
|
||||
|
||||
getRequestInstanceInfo(instanceId) {
|
||||
getRequestInstanceInfo(instanceId: number) {
|
||||
let instanceInfo = this.instanceInfos.get(instanceId);
|
||||
if (!instanceInfo) {
|
||||
throw new lib.RequestError(`Instance with ID ${instanceId} does not exist`);
|
||||
@@ -505,16 +519,16 @@ class Host extends lib.Link {
|
||||
/**
|
||||
* Initialize and connect an unloaded instance
|
||||
*
|
||||
* @param {number} instanceId - ID of instance to initialize.
|
||||
* @returns {Promise<module:host/host~InstanceConnection>} connection to instance.
|
||||
* @param instanceId - ID of instance to initialize.
|
||||
* @returns connection to instance.
|
||||
*/
|
||||
async _connectInstance(instanceId) {
|
||||
async _connectInstance(instanceId: number) {
|
||||
let instanceInfo = this.getRequestInstanceInfo(instanceId);
|
||||
if (this.instanceConnections.has(instanceId)) {
|
||||
throw new lib.RequestError(`Instance with ID ${instanceId} is running`);
|
||||
}
|
||||
|
||||
let hostAddress = new lib.Address(lib.Address.host, this.id);
|
||||
let hostAddress = new lib.Address(lib.Address.host, this.config.get("host.id"));
|
||||
let instanceAddress = new lib.Address(lib.Address.instance, instanceId);
|
||||
let [connectionClient, connectionServer] = lib.VirtualConnector.makePair(instanceAddress, hostAddress);
|
||||
let instanceConnection = new InstanceConnection(connectionServer, this, instanceId);
|
||||
@@ -554,12 +568,12 @@ class Host extends lib.Link {
|
||||
return { results };
|
||||
}
|
||||
|
||||
async fallbackInstanceListSavesRequest(request, src, dst) {
|
||||
async fallbackInstanceListSavesRequest(request: lib.InstanceListSavesRequest, src: lib.Address, dst: lib.Address) {
|
||||
let instanceInfo = this.getRequestInstanceInfo(dst.id);
|
||||
return await Instance.listSaves(path.join(instanceInfo.path, "saves"), null);
|
||||
}
|
||||
|
||||
async handleInstanceRenameSaveRequest(request) {
|
||||
async handleInstanceRenameSaveRequest(request: lib.InstanceRenameSaveRequest) {
|
||||
let { instanceId, oldName, newName } = request;
|
||||
checkRequestSaveName(oldName);
|
||||
checkRequestSaveName(newName);
|
||||
@@ -570,7 +584,7 @@ class Host extends lib.Link {
|
||||
path.join(instanceInfo.path, "saves", newName),
|
||||
{ overwrite: false },
|
||||
);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
throw new lib.RequestError(`${oldName} does not exist`);
|
||||
}
|
||||
@@ -579,7 +593,7 @@ class Host extends lib.Link {
|
||||
await this.sendSaveListUpdate(instanceId, path.join(instanceInfo.path, "saves"));
|
||||
}
|
||||
|
||||
async handleInstanceCopySaveRequest(request) {
|
||||
async handleInstanceCopySaveRequest(request: lib.InstanceCopySaveRequest) {
|
||||
let { instanceId, source, destination } = request;
|
||||
checkRequestSaveName(source);
|
||||
checkRequestSaveName(destination);
|
||||
@@ -590,7 +604,7 @@ class Host extends lib.Link {
|
||||
path.join(instanceInfo.path, "saves", destination),
|
||||
{ overwrite: false, errorOnExist: true },
|
||||
);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
throw new lib.RequestError(`${source} does not exist`);
|
||||
}
|
||||
@@ -599,7 +613,7 @@ class Host extends lib.Link {
|
||||
await this.sendSaveListUpdate(instanceId, path.join(instanceInfo.path, "saves"));
|
||||
}
|
||||
|
||||
async handleInstanceTransferSaveRequest(request) {
|
||||
async handleInstanceTransferSaveRequest(request: lib.InstanceTransferSaveRequest) {
|
||||
let { sourceName, targetName, copy, sourceInstanceId, targetInstanceId } = request;
|
||||
checkRequestSaveName(sourceName);
|
||||
checkRequestSaveName(targetName);
|
||||
@@ -626,7 +640,7 @@ class Host extends lib.Link {
|
||||
{ overwrite: true },
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
throw new lib.RequestError(`${sourceName} does not exist`);
|
||||
}
|
||||
@@ -638,9 +652,9 @@ class Host extends lib.Link {
|
||||
return targetName;
|
||||
}
|
||||
|
||||
async sendSaveListUpdate(instanceId, savesDir) {
|
||||
async sendSaveListUpdate(instanceId: number, savesDir: string) {
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
let saveList;
|
||||
let saveList: lib.SaveDetails[];
|
||||
if (instanceConnection) {
|
||||
saveList = await instanceConnection.send(new lib.InstanceListSavesRequest());
|
||||
} else {
|
||||
@@ -650,14 +664,14 @@ class Host extends lib.Link {
|
||||
this.send(new lib.InstanceSaveListUpdateEvent(instanceId, saveList));
|
||||
}
|
||||
|
||||
async handleInstanceDeleteSaveRequest(request) {
|
||||
async handleInstanceDeleteSaveRequest(request: lib.InstanceDeleteSaveRequest) {
|
||||
let { instanceId, name } = request;
|
||||
checkRequestSaveName(name);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instanceId);
|
||||
|
||||
try {
|
||||
await fs.unlink(path.join(instanceInfo.path, "saves", name));
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
throw new lib.RequestError(`${name} does not exist`);
|
||||
}
|
||||
@@ -666,7 +680,7 @@ class Host extends lib.Link {
|
||||
await this.sendSaveListUpdate(instanceId, path.join(instanceInfo.path, "saves"));
|
||||
}
|
||||
|
||||
async handleInstancePullSaveRequest(request) {
|
||||
async handleInstancePullSaveRequest(request: lib.InstancePullSaveRequest) {
|
||||
let { instanceId, streamId, name } = request;
|
||||
checkRequestSaveName(name);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instanceId);
|
||||
@@ -675,7 +689,7 @@ class Host extends lib.Link {
|
||||
url.pathname += `api/stream/${streamId}`;
|
||||
let response = await phin({
|
||||
url, method: "GET",
|
||||
core: { ca: this.tlsCa },
|
||||
core: { ca: this.tlsCa } as {},
|
||||
stream: true,
|
||||
});
|
||||
|
||||
@@ -686,13 +700,13 @@ class Host extends lib.Link {
|
||||
|
||||
let savesDir = path.join(instanceInfo.path, "saves");
|
||||
let tempFilename = name.replace(/(\.zip)?$/, ".tmp.zip");
|
||||
let writeStream;
|
||||
let writeStream: NodeJS.WritableStream;
|
||||
while (true) {
|
||||
try {
|
||||
writeStream = fs.createWriteStream(path.join(savesDir, tempFilename), { flags: "wx" });
|
||||
await events.once(writeStream, "open");
|
||||
break;
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "EEXIST") {
|
||||
tempFilename = await lib.findUnusedName(savesDir, tempFilename, ".tmp.zip");
|
||||
} else {
|
||||
@@ -710,16 +724,16 @@ class Host extends lib.Link {
|
||||
return name;
|
||||
}
|
||||
|
||||
async handleInstancePushSaveRequest(request) {
|
||||
async handleInstancePushSaveRequest(request: lib.InstancePushSaveRequest) {
|
||||
let { instanceId, streamId, name } = request;
|
||||
checkRequestSaveName(name);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instanceId);
|
||||
|
||||
let content;
|
||||
let content: Buffer;
|
||||
try {
|
||||
// phin doesn't support streaming requests :(
|
||||
content = await fs.readFile(path.join(instanceInfo.path, "saves", name));
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
throw new lib.RequestError(`${name} does not exist`);
|
||||
}
|
||||
@@ -730,19 +744,21 @@ class Host extends lib.Link {
|
||||
url.pathname += `api/stream/${streamId}`;
|
||||
phin({
|
||||
url, method: "PUT",
|
||||
core: { ca: this.tlsCa },
|
||||
core: { ca: this.tlsCa } as {},
|
||||
data: content,
|
||||
}).catch(err => {
|
||||
logger.error(`Error pushing save to controller:\n${err.stack}`, this.instanceLogMeta(instanceId));
|
||||
});
|
||||
}
|
||||
|
||||
async stopInstance(instanceId) {
|
||||
async stopInstance(instanceId: number) {
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
await instanceConnection.send(new lib.InstanceStopRequest());
|
||||
if (instanceConnection) {
|
||||
await instanceConnection.send(new lib.InstanceStopRequest());
|
||||
}
|
||||
}
|
||||
|
||||
async handleInstanceDeleteInternalRequest(request) {
|
||||
async handleInstanceDeleteInternalRequest(request: lib.InstanceDeleteInternalRequest) {
|
||||
let instanceId = request.instanceId;
|
||||
if (this.instanceConnections.has(instanceId)) {
|
||||
throw new lib.RequestError(`Instance with ID ${instanceId} is running`);
|
||||
@@ -784,8 +800,8 @@ class Host extends lib.Link {
|
||||
if (instanceInfo.config.get("instance.auto_start")) {
|
||||
try {
|
||||
let instanceConnection = await this._connectInstance(instanceId);
|
||||
await instanceConnection.send(new lib.InstanceStartRequest(null));
|
||||
} catch (err) {
|
||||
await instanceConnection.send(new lib.InstanceStartRequest());
|
||||
} catch (err: any) {
|
||||
logger.error(
|
||||
`Error during auto startup for ${instanceInfo.config.get("instance.name")}:\n${err.stack}`,
|
||||
this.instanceLogMeta(instanceId, instanceInfo)
|
||||
@@ -816,14 +832,14 @@ class Host extends lib.Link {
|
||||
for (let instanceId of this.instanceConnections.keys()) {
|
||||
try {
|
||||
await this.stopInstance(instanceId);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.error(`Unexpected error stopping instance:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connector.disconnect();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (!(err instanceof lib.SessionLost)) {
|
||||
logger.error(`Unexpected error preparing disconnect:\n${err.stack}`);
|
||||
}
|
||||
@@ -832,7 +848,7 @@ class Host extends lib.Link {
|
||||
try {
|
||||
// Clear silly interval in pidfile library.
|
||||
pidusage.clear();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
setBlocking(true);
|
||||
logger.error(`
|
||||
+------------------------------------------------------------+
|
||||
@@ -849,15 +865,11 @@ ${err.stack}`
|
||||
/**
|
||||
* True if the connection to the controller is connected, not in the dropped
|
||||
* state,and not in the process of disconnecting.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get connected() {
|
||||
return !this._disconnecting && this.connector.connected;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Host;
|
||||
|
||||
// For testing only
|
||||
module.exports._discoverInstances = discoverInstances;
|
||||
|
||||
export const _discoverInstances = discoverInstances;
|
||||
@@ -1,17 +1,17 @@
|
||||
"use strict";
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const pidusage = require("pidusage");
|
||||
const phin = require("phin");
|
||||
const util = require("util");
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import pidusage from "pidusage";
|
||||
import phin from "phin";
|
||||
import util from "util";
|
||||
import type { Static } from "@sinclair/typebox";
|
||||
|
||||
// internal libraries
|
||||
const lib = require("@clusterio/lib");
|
||||
const { PlayerStats } = lib;
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
const { FactorioServer } = require("./server");
|
||||
const { patch } = require("./patch");
|
||||
const { exportData } = require("./export");
|
||||
import { FactorioServer } from "./server";
|
||||
import { ModuleInfo, patch } from "./patch";
|
||||
import { exportData } from "./export";
|
||||
import type Host from "./Host"
|
||||
|
||||
|
||||
const instanceRconCommandDuration = new lib.Histogram(
|
||||
@@ -47,8 +47,8 @@ const instanceFactorioAutosaveSize = new lib.Gauge(
|
||||
{ labels: ["instance_id"] }
|
||||
);
|
||||
|
||||
function applyAsConfig(name) {
|
||||
return async function action(instance, value, logger) {
|
||||
function applyAsConfig(name: string) {
|
||||
return async function action(instance: Instance, value: unknown, logger: lib.Logger) {
|
||||
if (name === "tags" && value instanceof Array) {
|
||||
// Replace spaces with non-break spaces and delimit by spaces.
|
||||
// This does change the defined tags, but there doesn't seem to
|
||||
@@ -57,7 +57,7 @@ function applyAsConfig(name) {
|
||||
}
|
||||
try {
|
||||
await instance.sendRcon(`/config set ${name} ${value}`);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.error(`Error applying server setting ${name} ${err.message}`);
|
||||
}
|
||||
};
|
||||
@@ -78,10 +78,10 @@ const serverSettingsActions = {
|
||||
"game_password": applyAsConfig("password"),
|
||||
"require_user_verification": applyAsConfig("require-user-verification"),
|
||||
"tags": applyAsConfig("tags"),
|
||||
"visibility": async (instance, value, logger) => {
|
||||
"visibility": async (instance: Instance, value: unknown, logger: lib.Logger) => {
|
||||
for (let scope of ["lan", "public", "steam"]) {
|
||||
try {
|
||||
let enabled = Boolean(value[scope]);
|
||||
let enabled = Boolean((value as Record<string, string>)[scope]);
|
||||
await instance.sendRcon(`/config set visibility-${scope} ${enabled}`);
|
||||
} catch (err) {
|
||||
logger.error(`Error applying visibility ${scope} ${err}`);
|
||||
@@ -95,8 +95,46 @@ const serverSettingsActions = {
|
||||
* Keeps track of the runtime parameters of an instance
|
||||
* @alias module:host/src/Instance
|
||||
*/
|
||||
class Instance extends lib.Link {
|
||||
constructor(host, connector, dir, factorioDir, instanceConfig) {
|
||||
export default class Instance extends lib.Link {
|
||||
/**
|
||||
* ID of this instance, equivalenet to `instance.config.get("instance.id")`.
|
||||
*/
|
||||
readonly id: number
|
||||
plugins: Map<string, lib.BaseInstancePlugin>;
|
||||
config: lib.InstanceConfig;
|
||||
logger: lib.Logger;
|
||||
server: FactorioServer;
|
||||
/**
|
||||
* Mod pack currently running on this instance
|
||||
*/
|
||||
activeModPack = new lib.ModPack();
|
||||
/**
|
||||
* Per player statistics recorded by this instance.
|
||||
*/
|
||||
playerStats: Map<string, lib.PlayerStats> = new Map();
|
||||
/**
|
||||
* Players currently online on the instance.
|
||||
*/
|
||||
playersOnline: Set<string> = new Set();
|
||||
|
||||
|
||||
_host: Host;
|
||||
_dir: string;
|
||||
_configFieldChanged: (group: lib.ConfigGroup, field: string, prev: unknown) => void;
|
||||
_status: "stopped" | "starting" | "running" | "stopping" | "creating_save" | "exporting_data" = "stopped";
|
||||
_loadedSave: string | null = null;
|
||||
_playerCheckInterval: ReturnType<typeof setInterval> | undefined;
|
||||
_hadPlayersOnline = false;
|
||||
_playerAutosaveSlot = 1;
|
||||
|
||||
|
||||
constructor(
|
||||
host: Host,
|
||||
connector: lib.VirtualConnector,
|
||||
dir: string,
|
||||
factorioDir: string,
|
||||
instanceConfig: lib.InstanceConfig
|
||||
) {
|
||||
super(connector);
|
||||
this._host = host;
|
||||
this._dir = dir;
|
||||
@@ -104,10 +142,6 @@ class Instance extends lib.Link {
|
||||
this.plugins = new Map();
|
||||
this.config = instanceConfig;
|
||||
|
||||
/**
|
||||
* ID of this instance, equivalenet to `instance.config.get("instance.id")`.
|
||||
* @constant {number}
|
||||
*/
|
||||
this.id = this.config.get("instance.id");
|
||||
|
||||
this.logger = lib.logger.child({
|
||||
@@ -115,16 +149,16 @@ class Instance extends lib.Link {
|
||||
instance_name: this.name,
|
||||
});
|
||||
|
||||
this._configFieldChanged = (group, field, prev) => {
|
||||
this._configFieldChanged = (group: lib.ConfigGroup, field: string, prev: unknown) => {
|
||||
let hook = () => lib.invokeHook(this.plugins, "onInstanceConfigFieldChanged", group, field, prev);
|
||||
|
||||
if (group.name === "factorio" && field === "settings") {
|
||||
this.updateFactorioSettings(group.get(field), prev).finally(hook);
|
||||
this.updateFactorioSettings(group.get(field) as any, prev as any).finally(hook);
|
||||
} else if (group.name === "factorio" && field === "enable_whitelist") {
|
||||
this.updateFactorioWhitelist(group.get(field)).finally(hook);
|
||||
this.updateFactorioWhitelist(group.get(field) as any).finally(hook);
|
||||
} else {
|
||||
if (group.name === "factorio" && field === "max_concurrent_commands") {
|
||||
this.server.maxConcurrentCommands = group.get(field);
|
||||
this.server.maxConcurrentCommands = group.get(field) as number;
|
||||
}
|
||||
hook();
|
||||
}
|
||||
@@ -134,9 +168,9 @@ class Instance extends lib.Link {
|
||||
let serverOptions = {
|
||||
logger: this.logger,
|
||||
version: this.config.get("factorio.version"),
|
||||
gamePort: this.config.get("factorio.game_port"),
|
||||
rconPort: this.config.get("factorio.rcon_port"),
|
||||
rconPassword: this.config.get("factorio.rcon_password"),
|
||||
gamePort: this.config.get("factorio.game_port") ?? undefined,
|
||||
rconPort: this.config.get("factorio.rcon_port") ?? undefined,
|
||||
rconPassword: this.config.get("factorio.rcon_password") ?? undefined,
|
||||
enableWhitelist: this.config.get("factorio.enable_whitelist"),
|
||||
enableAuthserverBans: this.config.get("factorio.enable_authserver_bans"),
|
||||
verboseLogging: this.config.get("factorio.verbose_logging"),
|
||||
@@ -144,9 +178,6 @@ class Instance extends lib.Link {
|
||||
maxConcurrentCommands: this.config.get("factorio.max_concurrent_commands"),
|
||||
};
|
||||
|
||||
// Valid statuses are stopped, starting, running, stopping, creating_save and exporting_data.
|
||||
this._status = "stopped";
|
||||
this._loadedSave = null;
|
||||
this.server = new FactorioServer(
|
||||
factorioDir, this._dir, serverOptions
|
||||
);
|
||||
@@ -185,27 +216,6 @@ class Instance extends lib.Link {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mod pack currently running on this instance
|
||||
* @type {module:lib.ModPack}
|
||||
*/
|
||||
this.activeModPack = undefined;
|
||||
|
||||
/**
|
||||
* Per player statistics recorded by this instance.
|
||||
* @type {Map<string, module:lib.PlayerStats>}
|
||||
*/
|
||||
this.playerStats = new Map();
|
||||
|
||||
/**
|
||||
* Players currently online on the instance.
|
||||
* @type {Map<string, string>}
|
||||
*/
|
||||
this.playersOnline = new Set();
|
||||
this._playerCheckInterval = null;
|
||||
this._hadPlayersOnline = false;
|
||||
this._playerAutosaveSlot = 1;
|
||||
|
||||
this.handle(lib.InstanceExtractPlayersRequest, this.handleInstanceExtractPlayersRequest.bind(this));
|
||||
this.handle(lib.InstanceAdminlistUpdateEvent, this.handleInstanceAdminlistUpdateEvent.bind(this));
|
||||
this.handle(lib.InstanceBanlistUpdateEvent, this.handleInstanceBanlistUpdateEvent.bind(this));
|
||||
@@ -226,12 +236,12 @@ class Instance extends lib.Link {
|
||||
}
|
||||
|
||||
_watchPlayerJoinsByChat() {
|
||||
this.server.on("output", (parsed, line) => {
|
||||
this.server.on("output", (parsed: lib.ParsedFactorioOutput, line: string) => {
|
||||
if (parsed.type !== "action") {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = /^([^ ]+)/.exec(parsed.message)[1];
|
||||
let name = /^([^ ]+)/.exec(parsed.message)![1];
|
||||
if (parsed.action === "JOIN") {
|
||||
this._recordPlayerJoin(name);
|
||||
} else if (["LEAVE", "KICK", "BAN"].includes(parsed.action)) {
|
||||
@@ -239,7 +249,7 @@ class Instance extends lib.Link {
|
||||
"LEAVE": "quit",
|
||||
"KICK": "kicked",
|
||||
"BAN": "banned",
|
||||
}[parsed.action];
|
||||
}[parsed.action]!;
|
||||
this._recordPlayerLeave(name, reason);
|
||||
}
|
||||
});
|
||||
@@ -278,7 +288,7 @@ class Instance extends lib.Link {
|
||||
}
|
||||
}
|
||||
|
||||
_recordPlayerJoin(name) {
|
||||
_recordPlayerJoin(name: string) {
|
||||
if (this.playersOnline.has(name)) {
|
||||
return;
|
||||
}
|
||||
@@ -286,7 +296,7 @@ class Instance extends lib.Link {
|
||||
|
||||
let stats = this.playerStats.get(name);
|
||||
if (!stats) {
|
||||
stats = new PlayerStats();
|
||||
stats = new lib.PlayerStats();
|
||||
this.playerStats.set(name, stats);
|
||||
}
|
||||
stats.lastJoinAt = new Date();
|
||||
@@ -302,15 +312,15 @@ class Instance extends lib.Link {
|
||||
lib.invokeHook(this.plugins, "onPlayerEvent", event);
|
||||
}
|
||||
|
||||
_recordPlayerLeave(name, reason) {
|
||||
_recordPlayerLeave(name: string, reason: string) {
|
||||
if (!this.playersOnline.delete(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stats = this.playerStats.get(name);
|
||||
let stats = this.playerStats.get(name)!;
|
||||
stats.lastLeaveAt = new Date();
|
||||
stats.lastLeaveReason = reason;
|
||||
stats.onlineTimeMs += stats.lastLeaveAt.getTime() - stats.lastJoinAt.getTime();
|
||||
stats.onlineTimeMs += stats.lastLeaveAt.getTime() - stats.lastJoinAt!.getTime();
|
||||
this._hadPlayersOnline = true;
|
||||
|
||||
let event = {
|
||||
@@ -331,13 +341,13 @@ 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));
|
||||
let playerTimes: Record<string, number> = JSON.parse(await this.sendRcon(exportPlayerTimes));
|
||||
let count = 0;
|
||||
|
||||
for (let [name, onlineTimeTicks] of Object.entries(playerTimes)) {
|
||||
let stats = this.playerStats.get(name);
|
||||
if (!stats) {
|
||||
stats = new PlayerStats();
|
||||
stats = new lib.PlayerStats();
|
||||
this.playerStats.set(name, stats);
|
||||
}
|
||||
stats.onlineTimeMs = onlineTimeTicks * 1000 / 60;
|
||||
@@ -355,7 +365,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
this.logger.info(`Extracted data for ${count} player(s)`);
|
||||
}
|
||||
|
||||
async sendRcon(message, expectEmpty, plugin = "") {
|
||||
async sendRcon(message: string, expectEmpty = false, plugin = "") {
|
||||
let instanceId = String(this.id);
|
||||
let observeDuration = instanceRconCommandDuration.labels(instanceId).startTimer();
|
||||
try {
|
||||
@@ -366,7 +376,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
}
|
||||
}
|
||||
|
||||
static async listSaves(savesDir, loadedSave) {
|
||||
static async listSaves(savesDir: string, loadedSave: string | null) {
|
||||
let defaultSave = null;
|
||||
if (loadedSave === null) {
|
||||
defaultSave = await lib.getNewestFile(
|
||||
@@ -374,9 +384,9 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
);
|
||||
}
|
||||
|
||||
let list = [];
|
||||
let list: lib.SaveDetails[] = [];
|
||||
for (let name of await fs.readdir(savesDir)) {
|
||||
let type;
|
||||
let type: "file" | "directory" | "special";
|
||||
let stat = await fs.stat(path.join(savesDir, name));
|
||||
if (stat.isFile()) {
|
||||
type = "file";
|
||||
@@ -409,7 +419,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
);
|
||||
}
|
||||
|
||||
async _autosave(name) {
|
||||
async _autosave(name: string) {
|
||||
let stat = await fs.stat(this.path("saves", `${name}.zip`));
|
||||
instanceFactorioAutosaveSize.labels(String(this.id)).set(stat.size);
|
||||
|
||||
@@ -431,7 +441,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
await this.sendSaveListUpdate();
|
||||
}
|
||||
|
||||
notifyStatus(status) {
|
||||
notifyStatus(status: Instance["_status"]) {
|
||||
this._status = status;
|
||||
this.sendTo(
|
||||
"controller",
|
||||
@@ -447,8 +457,6 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
* Current state of the instance
|
||||
*
|
||||
* One of stopped, starting, running, stopping, creating_save and exporting_data
|
||||
*
|
||||
* @returns {string} instance status.
|
||||
*/
|
||||
get status() {
|
||||
return this._status;
|
||||
@@ -482,7 +490,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
this._saveStats().catch(err => this.logger.error(`Error saving stats:\n${err.stack}`));
|
||||
}
|
||||
|
||||
async _loadPlugin(pluginInfo, host) {
|
||||
async _loadPlugin(pluginInfo: lib.PluginInfo, host: Host) {
|
||||
let pluginLoadStarted = Date.now();
|
||||
let InstancePluginClass = await lib.loadInstancePluginClass(pluginInfo);
|
||||
let instancePlugin = new InstancePluginClass(pluginInfo, this, host);
|
||||
@@ -495,14 +503,16 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
async _loadStats() {
|
||||
let instanceStats;
|
||||
try {
|
||||
instanceStats = JSON.parse(await fs.readFile(this.path("instance-stats.json")));
|
||||
} catch (err) {
|
||||
instanceStats = JSON.parse(await fs.readFile(this.path("instance-stats.json"), "utf8"));
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
this.playerStats = new Map(instanceStats["players"].map(([id, stats]) => [id, new PlayerStats(stats)]));
|
||||
this.playerStats = new Map(instanceStats["players"].map(
|
||||
([id, stats]: [number, Static<typeof lib.PlayerStats.jsonSchema>]) => [id, new lib.PlayerStats(stats)])
|
||||
);
|
||||
this._playerAutosaveSlot = instanceStats["player_autosave_slot"] || 1;
|
||||
}
|
||||
|
||||
@@ -514,7 +524,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
await lib.safeOutputFile(this.path("instance-stats.json"), content);
|
||||
}
|
||||
|
||||
async init(pluginInfos) {
|
||||
async init(pluginInfos: lib.PluginInfo[]) {
|
||||
this.notifyStatus("starting");
|
||||
try {
|
||||
await this._loadStats();
|
||||
@@ -544,7 +554,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
}
|
||||
}
|
||||
|
||||
let plugins = {};
|
||||
let plugins: Record<string, string> = {};
|
||||
for (let [name, plugin] of this.plugins) {
|
||||
plugins[name] = plugin.info.version;
|
||||
}
|
||||
@@ -557,11 +567,11 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
* Use the example settings as the basis and override it with all the
|
||||
* entries from the given settings object.
|
||||
*
|
||||
* @param {Object} overrides - Server settings to override.
|
||||
* @returns {Promise<Object>}
|
||||
* @param overrides - Server settings to override.
|
||||
* @returns
|
||||
* server example settings with the given settings applied over it.
|
||||
*/
|
||||
async resolveServerSettings(overrides) {
|
||||
async resolveServerSettings(overrides: Record<string, unknown>) {
|
||||
let serverSettings = await this.server.exampleSettings();
|
||||
|
||||
for (let [key, value] of Object.entries(overrides)) {
|
||||
@@ -595,11 +605,11 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
* Creates the neccessary files for starting up a new instance into the
|
||||
* provided instance directory.
|
||||
*
|
||||
* @param {String} instanceDir -
|
||||
* @param instanceDir -
|
||||
* Directory to create the new instance into.
|
||||
* @param {String} factorioDir - Path to factorio installation.
|
||||
* @param factorioDir - Path to factorio installation.
|
||||
*/
|
||||
static async create(instanceDir, factorioDir) {
|
||||
static async create(instanceDir: string, factorioDir: string) {
|
||||
await fs.ensureDir(path.join(instanceDir, "script-output"));
|
||||
await fs.ensureDir(path.join(instanceDir, "saves"));
|
||||
}
|
||||
@@ -722,12 +732,12 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
*
|
||||
* Creates a new save if no save is passed and patches it with modules.
|
||||
*
|
||||
* @param {string|undefined} saveName -
|
||||
* @param saveName -
|
||||
* Save to prepare from the instance saves directory. Creates a new
|
||||
* save if null.
|
||||
* @returns {Promise<string>} Name of the save prepared.
|
||||
* @returns Name of the save prepared.
|
||||
*/
|
||||
async prepareSave(saveName) {
|
||||
async prepareSave(saveName?: string) {
|
||||
// Use latest save if no save was specified
|
||||
if (saveName === undefined) {
|
||||
saveName = await lib.getNewestFile(
|
||||
@@ -766,8 +776,16 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
// Patch save with lua modules from plugins
|
||||
this.logger.verbose("Patching save");
|
||||
|
||||
interface ModuleJson {
|
||||
version: string,
|
||||
name: string;
|
||||
load?: string[];
|
||||
require?: string[];
|
||||
dependencies?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Find plugin modules to patch in
|
||||
let modules = new Map();
|
||||
let modules: Map<string, ModuleInfo> = new Map();
|
||||
for (let [pluginName, plugin] of this.plugins) {
|
||||
let pluginPackagePath = require.resolve(path.posix.join(plugin.info.requirePath, "package.json"));
|
||||
let modulePath = path.join(path.dirname(pluginPackagePath), "module");
|
||||
@@ -780,30 +798,29 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
throw new Error(`Module for plugin ${pluginName} is missing module.json`);
|
||||
}
|
||||
|
||||
let module;
|
||||
let module: Omit<ModuleJson, "version">;
|
||||
try {
|
||||
module = JSON.parse(await fs.readFile(moduleJsonPath));
|
||||
} catch (err) {
|
||||
module = JSON.parse(await fs.readFile(moduleJsonPath, "utf8"));
|
||||
} catch (err: any) {
|
||||
throw new Error(`Loading module/module.json in plugin ${pluginName} failed: ${err.message}`);
|
||||
}
|
||||
if (module.name !== pluginName) {
|
||||
throw new Error(`Expected name of module for plugin ${pluginName} to match the plugin name`);
|
||||
}
|
||||
|
||||
module = {
|
||||
modules.set(module.name, {
|
||||
version: plugin.info.version,
|
||||
dependencies: { "clusterio": "*" },
|
||||
path: modulePath,
|
||||
load: [],
|
||||
require: [],
|
||||
...module,
|
||||
};
|
||||
modules.set(module.name, module);
|
||||
});
|
||||
}
|
||||
|
||||
// Find stand alone modules to load
|
||||
// XXX for now only the included clusterio module is loaded
|
||||
let modulesDirectory = path.join(__dirname, "..", "modules");
|
||||
let modulesDirectory = path.join(__dirname, "..", "..", "modules");
|
||||
for (let entry of await fs.readdir(modulesDirectory, { withFileTypes: true })) {
|
||||
if (!entry.isFile()) {
|
||||
if (modules.has(entry.name)) {
|
||||
@@ -815,19 +832,18 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
throw new Error(`Module ${entry.name} is missing module.json`);
|
||||
}
|
||||
|
||||
let module = JSON.parse(await fs.readFile(moduleJsonPath));
|
||||
let module: ModuleJson = JSON.parse(await fs.readFile(moduleJsonPath, "utf8"));
|
||||
if (module.name !== entry.name) {
|
||||
throw new Error(`Expected name of module ${entry.name} to match the directory name`);
|
||||
}
|
||||
|
||||
module = {
|
||||
modules.set(module.name, {
|
||||
path: path.join(modulesDirectory, entry.name),
|
||||
dependencies: { "clusterio": "*" },
|
||||
load: [],
|
||||
require: [],
|
||||
...module,
|
||||
};
|
||||
modules.set(module.name, module);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -840,9 +856,9 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
*
|
||||
* Launches the Factorio server for this instance with the given save.
|
||||
*
|
||||
* @param {String} saveName - Name of save game to load.
|
||||
* @param saveName - Name of save game to load.
|
||||
*/
|
||||
async start(saveName) {
|
||||
async start(saveName: string) {
|
||||
this.server.on("rcon-ready", () => {
|
||||
this.logger.verbose("RCON connection established");
|
||||
});
|
||||
@@ -870,12 +886,12 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
* Launches the Factorio server for this instance with the given
|
||||
* scenario.
|
||||
*
|
||||
* @param {String} scenario - Name of scenario to load.
|
||||
* @param {?number} seed - seed to use.
|
||||
* @param {?object} mapGenSettings - MapGenSettings to use.
|
||||
* @param {?object} mapSettings - MapSettings to use.
|
||||
* @param scenario - Name of scenario to load.
|
||||
* @param seed - seed to use.
|
||||
* @param mapGenSettings - MapGenSettings to use.
|
||||
* @param mapSettings - MapSettings to use.
|
||||
*/
|
||||
async startScenario(scenario, seed, mapGenSettings, mapSettings) {
|
||||
async startScenario(scenario: string, seed?: number, mapGenSettings?: object, mapSettings?: object) {
|
||||
this.server.on("rcon-ready", () => {
|
||||
this.logger.verbose("RCON connection established");
|
||||
});
|
||||
@@ -897,7 +913,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
await this.sendRcon(`/sc clusterio_private.update_instance(${this.id}, "${name}")`, true);
|
||||
}
|
||||
|
||||
async updateFactorioSettings(current, previous) {
|
||||
async updateFactorioSettings(current: Record<string, unknown>, previous: Record<string, unknown>) {
|
||||
current = await this.resolveServerSettings(current);
|
||||
previous = await this.resolveServerSettings(previous);
|
||||
|
||||
@@ -911,10 +927,10 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
/**
|
||||
* Enable or disable the player whitelist
|
||||
*
|
||||
* @param {boolean} enable -
|
||||
* @param enable -
|
||||
* True to enable the whitelist, False to disable the whitelist.
|
||||
*/
|
||||
async updateFactorioWhitelist(enable) {
|
||||
async updateFactorioWhitelist(enable: boolean) {
|
||||
if (!enable) {
|
||||
await this.sendRcon("/whitelist disable");
|
||||
}
|
||||
@@ -931,7 +947,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
}
|
||||
}
|
||||
|
||||
async handleInstanceAdminlistUpdateEvent(request) {
|
||||
async handleInstanceAdminlistUpdateEvent(request: lib.InstanceAdminlistUpdateEvent) {
|
||||
if (!this.config.get("factorio.sync_adminlist")) {
|
||||
return;
|
||||
}
|
||||
@@ -941,7 +957,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
await this.sendRcon(command);
|
||||
}
|
||||
|
||||
async handleInstanceBanlistUpdateEvent(request) {
|
||||
async handleInstanceBanlistUpdateEvent(request: lib.InstanceBanlistUpdateEvent) {
|
||||
if (!this.config.get("factorio.sync_banlist")) {
|
||||
return;
|
||||
}
|
||||
@@ -951,7 +967,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
await this.sendRcon(command);
|
||||
}
|
||||
|
||||
async handleInstanceWhitelistUpdateEvent(request) {
|
||||
async handleInstanceWhitelistUpdateEvent(request: lib.InstanceWhitelistUpdateEvent) {
|
||||
if (!this.config.get("factorio.sync_whitelist")) {
|
||||
return;
|
||||
}
|
||||
@@ -985,7 +1001,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
await this.server.kill(true);
|
||||
}
|
||||
|
||||
async handleControllerConnectionEvent(event) {
|
||||
async handleControllerConnectionEvent(event: lib.ControllerConnectionEvent) {
|
||||
await lib.invokeHook(this.plugins, "onControllerConnectionEvent", event.event);
|
||||
}
|
||||
|
||||
@@ -998,7 +1014,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
if (!["stopped", "stopping"].includes(this._status)) {
|
||||
let pluginResults = await lib.invokeHook(this.plugins, "onMetrics");
|
||||
for (let metricIterator of pluginResults) {
|
||||
for await (let metric of metricIterator) {
|
||||
for await (let metric of metricIterator as any) {
|
||||
results.push(lib.serializeResult(metric));
|
||||
}
|
||||
}
|
||||
@@ -1014,7 +1030,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
return new lib.InstanceMetricsRequest.Response(results);
|
||||
}
|
||||
|
||||
async handleInstanceStartRequest(request) {
|
||||
async handleInstanceStartRequest(request: lib.InstanceStartRequest) {
|
||||
let saveName = request.save;
|
||||
try {
|
||||
await this.prepare();
|
||||
@@ -1033,7 +1049,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
}
|
||||
}
|
||||
|
||||
async handleInstanceLoadScenarioRequest(request) {
|
||||
async handleInstanceLoadScenarioRequest(request: lib.InstanceLoadScenarioRequest) {
|
||||
if (this.config.get("factorio.enable_save_patching")) {
|
||||
this.notifyExit();
|
||||
throw new lib.RequestError("Load scenario cannot be used with save patching enabled");
|
||||
@@ -1060,7 +1076,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
return await Instance.listSaves(this.path("saves"), this._loadedSave);
|
||||
}
|
||||
|
||||
async handleInstanceCreateSaveRequest(request) {
|
||||
async handleInstanceCreateSaveRequest(request: lib.InstanceCreateSaveRequest) {
|
||||
this.notifyStatus("creating_save");
|
||||
try {
|
||||
this.logger.verbose("Writing server-settings.json");
|
||||
@@ -1095,11 +1111,11 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
let content = await zip.generateAsync({ type: "nodebuffer" });
|
||||
let url = new URL(this._host.config.get("host.controller_url"));
|
||||
url.pathname += "api/upload-export";
|
||||
url.searchParams.set("mod_pack_id", this.activeModPack.id);
|
||||
url.searchParams.set("mod_pack_id", String(this.activeModPack.id));
|
||||
let response = await phin({
|
||||
url, method: "PUT",
|
||||
data: content,
|
||||
core: { ca: this._host.tlsCa },
|
||||
core: { ca: this._host.tlsCa } as {},
|
||||
headers: {
|
||||
"Content-Type": "application/zip",
|
||||
"x-access-token": this._host.config.get("host.controller_token"),
|
||||
@@ -1122,7 +1138,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
await this.kill();
|
||||
}
|
||||
|
||||
async handleInstanceSendRconRequest(request) {
|
||||
async handleInstanceSendRconRequest(request: lib.InstanceSendRconRequest) {
|
||||
return await this.sendRcon(request.command);
|
||||
}
|
||||
|
||||
@@ -1143,11 +1159,9 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
* returns a path to the mods directory of the instance. If no parts are
|
||||
* given it returns a path to the directory of the instance.
|
||||
*
|
||||
* @returns {string} path in instance directory.
|
||||
* @returns path in instance directory.
|
||||
*/
|
||||
path(...parts) {
|
||||
path(...parts: string[]) {
|
||||
return path.join(this._dir, ...parts);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Instance;
|
||||
@@ -1,40 +0,0 @@
|
||||
"use strict";
|
||||
const lib = require("@clusterio/lib");
|
||||
|
||||
|
||||
class InstanceConnection extends lib.Link {
|
||||
constructor(connector, host, instanceId) {
|
||||
super(connector);
|
||||
this.host = host;
|
||||
this.router = this.host.router;
|
||||
this.instanceId = instanceId;
|
||||
this.plugins = new Map();
|
||||
this.status = "stopped";
|
||||
|
||||
this.handle(lib.InstanceInitialisedEvent, this.handleInstanceInitialisedEvent.bind(this));
|
||||
this.snoopEvent(lib.InstanceStatusChangedEvent, this.snoopInstanceStatusChangedEvent.bind(this));
|
||||
}
|
||||
|
||||
async broadcastEventToInstance(message, event) {
|
||||
for (let instanceConnection of this.host.instanceConnections.values()) {
|
||||
// Do not broadcast back to the source
|
||||
if (instanceConnection === this) { continue; }
|
||||
if (event.plugin && !instanceConnection.plugins.has(event.plugin)) { continue; }
|
||||
|
||||
event.send(instanceConnection, message.data);
|
||||
}
|
||||
}
|
||||
|
||||
async handleInstanceInitialisedEvent(event) {
|
||||
this.plugins = new Map(Object.entries(event.plugins));
|
||||
}
|
||||
|
||||
async snoopInstanceStatusChangedEvent(event) {
|
||||
this.status = event.status;
|
||||
if (this.status === "stopped") {
|
||||
this.host.instanceConnections.delete(this.instanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InstanceConnection;
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import type Host from "./Host";
|
||||
import type Instance from "./Instance";
|
||||
|
||||
|
||||
export default class InstanceConnection extends lib.Link {
|
||||
plugins = new Map<string, string>();
|
||||
status: Instance["status"] = "stopped";
|
||||
|
||||
constructor(
|
||||
connector: lib.VirtualConnector,
|
||||
public host: Host,
|
||||
public instanceId: number
|
||||
) {
|
||||
super(connector);
|
||||
this.router = this.host.router;
|
||||
|
||||
this.handle(lib.InstanceInitialisedEvent, this.handleInstanceInitialisedEvent.bind(this));
|
||||
this.snoopEvent(lib.InstanceStatusChangedEvent, this.snoopInstanceStatusChangedEvent.bind(this));
|
||||
}
|
||||
|
||||
async handleInstanceInitialisedEvent(event: lib.InstanceInitialisedEvent) {
|
||||
this.plugins = new Map(Object.entries(event.plugins));
|
||||
}
|
||||
|
||||
async snoopInstanceStatusChangedEvent(event: lib.InstanceStatusChangedEvent) {
|
||||
this.status = event.status as Instance["status"];
|
||||
if (this.status === "stopped") {
|
||||
this.host.instanceConnections.delete(this.instanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,43 @@
|
||||
// Export of item icons and locale
|
||||
"use strict";
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const Jimp = require("jimp");
|
||||
const JSZip = require("jszip");
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import Jimp from "jimp";
|
||||
import JSZip from "jszip";
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
const libBuildMod = require("@clusterio/lib/build_mod");
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as libBuildMod from "@clusterio/lib/dist/build_mod";
|
||||
import type { FactorioServer } from "./server";
|
||||
|
||||
interface Prototype {
|
||||
[index: string]: unknown,
|
||||
type: string,
|
||||
name: string,
|
||||
}
|
||||
interface SimpleIconSpecification {
|
||||
icon: string;
|
||||
icon_size: number;
|
||||
icon_mipmaps?: number;
|
||||
}
|
||||
type Color = { r: number, g: number, b: number, a: number };
|
||||
interface IconLayer {
|
||||
icon: string;
|
||||
icon_size: number;
|
||||
tint?: Color,
|
||||
shift?: [number, number],
|
||||
scale?: number,
|
||||
icon_mipmaps?: number,
|
||||
}
|
||||
interface LayeredIconSpecification {
|
||||
icons: IconLayer[];
|
||||
icon_size: number;
|
||||
icon_mipmaps?: number;
|
||||
}
|
||||
type IconSpecification = LayeredIconSpecification | SimpleIconSpecification;
|
||||
type ItemPrototype = Prototype & IconSpecification;
|
||||
|
||||
|
||||
type Prototypes = Record<string, Record<string, Prototype>>
|
||||
|
||||
/**
|
||||
* Generate the export mod needed for exportData
|
||||
@@ -15,11 +46,10 @@ const libBuildMod = require("@clusterio/lib/build_mod");
|
||||
* the server given, with dependencies generated for all the mods present in
|
||||
* the server's mods folder.
|
||||
*
|
||||
* @param {module:host/src/server.FactorioServer} server -
|
||||
* @param server -
|
||||
* The server to generate the export mod for.
|
||||
* @memberof module:lib
|
||||
*/
|
||||
async function generateExportMod(server) {
|
||||
async function generateExportMod(server: FactorioServer) {
|
||||
let dependencies = [];
|
||||
let splitter = /^(.*)_(\d+\.\d+\.\d+)(\.zip)?$/;
|
||||
for (let entry of await fs.readdir(server.writePath("mods"))) {
|
||||
@@ -33,7 +63,7 @@ async function generateExportMod(server) {
|
||||
clean: false,
|
||||
build: true,
|
||||
pack: true,
|
||||
sourceDir: path.join(__dirname, "..", "lua", "export"),
|
||||
sourceDir: path.join(__dirname, "..", "..", "lua", "export"),
|
||||
outputDir: server.writePath("mods"),
|
||||
bumpPatch: false,
|
||||
factorioVersion: server.version.replace(/\.\d+$/, ""),
|
||||
@@ -42,7 +72,7 @@ async function generateExportMod(server) {
|
||||
}
|
||||
|
||||
let zipCache = new Map();
|
||||
async function loadZip(server, modVersions, mod) {
|
||||
async function loadZip(server: FactorioServer, modVersions: Map<string, string>, mod: string) {
|
||||
let modVersion = modVersions.get(mod);
|
||||
if (!modVersion) {
|
||||
throw new Error(`Got path for unknown mod ${mod}`);
|
||||
@@ -61,16 +91,13 @@ async function loadZip(server, modVersions, mod) {
|
||||
/**
|
||||
* Load the given Factorio file path into a Buffer
|
||||
*
|
||||
* @param {module:host/src/server.FactorioServer} server -
|
||||
* The server to load the file from.
|
||||
* @param {Map<string, string>} modVersions - Mapping of mod to version used.
|
||||
* @param {string} modPath - Factorio style path to the file to load.
|
||||
* @returns {Promise<?Buffer>} The content of the file or null if not found.
|
||||
* @memberof module:lib
|
||||
* @private
|
||||
* @inner
|
||||
* @param server - The server to load the file from.
|
||||
* @param modVersions - Mapping of mod to version used.
|
||||
* @param modPath - Factorio style path to the file to load.
|
||||
* @returns The content of the file or null if not found.
|
||||
* @internal
|
||||
*/
|
||||
async function loadFile(server, modVersions, modPath) {
|
||||
async function loadFile(server: FactorioServer, modVersions: Map<string, string>, modPath: string) {
|
||||
let match = /^__([^\/]+)__\/(.*)$/.exec(modPath);
|
||||
if (!match) {
|
||||
throw new Error(`Bad mod path ${match}`);
|
||||
@@ -81,7 +108,7 @@ async function loadFile(server, modVersions, modPath) {
|
||||
if (["core", "base"].includes(mod)) {
|
||||
try {
|
||||
return await fs.readFile(server.dataPath(mod, filePath));
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
@@ -92,7 +119,7 @@ async function loadFile(server, modVersions, modPath) {
|
||||
let zip;
|
||||
try {
|
||||
zip = await loadZip(server, modVersions, mod);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
@@ -107,7 +134,15 @@ async function loadFile(server, modVersions, modPath) {
|
||||
return await file.async("nodebuffer");
|
||||
}
|
||||
|
||||
async function loadIcon(server, modVersions, iconPath, iconSize, iconMipmaps, iconCache) {
|
||||
type IconCache = Map<string, Jimp | null>;
|
||||
async function loadIcon(
|
||||
server: FactorioServer,
|
||||
modVersions: Map<string, string>,
|
||||
iconPath: string,
|
||||
iconSize: number,
|
||||
iconMipmaps: number,
|
||||
iconCache: IconCache,
|
||||
) {
|
||||
let icon = iconCache.get(iconPath);
|
||||
if (icon === undefined) {
|
||||
let fileContent = await loadFile(server, modVersions, iconPath);
|
||||
@@ -124,7 +159,13 @@ async function loadIcon(server, modVersions, iconPath, iconSize, iconMipmaps, ic
|
||||
return icon;
|
||||
}
|
||||
|
||||
async function loadSimpleIcon(server, modVersions, item, size, iconCache) {
|
||||
async function loadSimpleIcon(
|
||||
server: FactorioServer,
|
||||
modVersions: Map<string, string>,
|
||||
item: SimpleIconSpecification,
|
||||
size: number,
|
||||
iconCache: IconCache,
|
||||
) {
|
||||
let icon = await loadIcon(server, modVersions, item.icon, item.icon_size, 0, iconCache);
|
||||
if (icon) {
|
||||
let iconScale = size / item.icon_size;
|
||||
@@ -136,7 +177,13 @@ async function loadSimpleIcon(server, modVersions, item, size, iconCache) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
async function loadLayeredIcon(server, modVersions, item, size, iconCache) {
|
||||
async function loadLayeredIcon(
|
||||
server: FactorioServer,
|
||||
modVersions: Map<string, string>,
|
||||
item: LayeredIconSpecification,
|
||||
size: number,
|
||||
iconCache: IconCache,
|
||||
) {
|
||||
let baseLayerSize = item.icons[0].icon_size || item.icon_size;
|
||||
let icon = await Jimp.create(size, size);
|
||||
|
||||
@@ -156,7 +203,7 @@ async function loadLayeredIcon(server, modVersions, item, size, iconCache) {
|
||||
|
||||
iconLayer = iconLayer.clone();
|
||||
|
||||
let tint;
|
||||
let tint: Color;
|
||||
if (layer.tint) {
|
||||
let divisor = (layer.tint.r > 1 || layer.tint.g > 1 || layer.tint.b > 1) ? 255 : 1;
|
||||
tint = {
|
||||
@@ -188,7 +235,7 @@ async function loadLayeredIcon(server, modVersions, item, size, iconCache) {
|
||||
if (x < 0 || x >= size || y < 0 || y >= size) {
|
||||
return;
|
||||
}
|
||||
let sdata = iconLayer.bitmap.data;
|
||||
let sdata = iconLayer!.bitmap.data;
|
||||
let ddata = icon.bitmap.data;
|
||||
let didx = icon.getPixelIndex(x, y);
|
||||
|
||||
@@ -235,10 +282,10 @@ const itemTypes = new Set([
|
||||
// TODO consider adding virtual signals and recipes
|
||||
]);
|
||||
|
||||
function filterItems(prototypes) {
|
||||
function filterItems(prototypes: Prototypes): ItemPrototype[] {
|
||||
return Object.entries(prototypes)
|
||||
.filter(([type, _]) => itemTypes.has(type))
|
||||
.flatMap(([_, typePrototypes]) => Object.values(typePrototypes))
|
||||
.flatMap(([_, typePrototypes]) => Object.values(typePrototypes)) as ItemPrototype[]
|
||||
;
|
||||
}
|
||||
|
||||
@@ -248,18 +295,15 @@ function filterItems(prototypes) {
|
||||
* Assembles and packs the icons for the item prototypes given into a single
|
||||
* spritesheet and json file with meta data.
|
||||
*
|
||||
* @param {module:host/src/server.FactorioServer} server -
|
||||
* @param server -
|
||||
* The server to generate the export mod for.
|
||||
* @param {Map<string, string>} modVersions -
|
||||
* @param modVersions -
|
||||
* Mapping of mod name to versions to get icons from.
|
||||
* @param {Array<Object>} items - Array of item prototypes.
|
||||
* @return {Promise<{itemSheet: Jimp, itemData: Map<string,Object>}>}
|
||||
* Item spritesheet and metadata.
|
||||
* @memberof module:lib
|
||||
* @private
|
||||
* @inner
|
||||
* @param items - Array of item prototypes.
|
||||
* @return Item spritesheet and metadata.
|
||||
* @internal
|
||||
*/
|
||||
async function exportItems(server, modVersions, items) {
|
||||
async function exportItems(server: FactorioServer, modVersions: Map<string, string>, items: ItemPrototype[]) {
|
||||
|
||||
// Size to render icons at
|
||||
let size = 32;
|
||||
@@ -272,19 +316,19 @@ async function exportItems(server, modVersions, items) {
|
||||
let itemData = new Map();
|
||||
let pos = 0;
|
||||
|
||||
let iconCache = new Map();
|
||||
let iconCache: IconCache = new Map();
|
||||
let simpleIcons = new Map();
|
||||
for (let item of items) {
|
||||
let icon;
|
||||
let iconPos;
|
||||
let icon: Jimp | null = null;
|
||||
let iconPos: number | undefined;
|
||||
if (item.icons) {
|
||||
icon = await loadLayeredIcon(server, modVersions, item, size, iconCache);
|
||||
icon = await loadLayeredIcon(server, modVersions, item as LayeredIconSpecification, size, iconCache);
|
||||
iconPos = pos;
|
||||
|
||||
} else {
|
||||
iconPos = simpleIcons.get(item.icon);
|
||||
if (iconPos === undefined) {
|
||||
icon = await loadSimpleIcon(server, modVersions, item, size, iconCache);
|
||||
icon = await loadSimpleIcon(server, modVersions, item as SimpleIconSpecification, size, iconCache);
|
||||
if (icon) {
|
||||
iconPos = pos;
|
||||
simpleIcons.set(item.icon, pos);
|
||||
@@ -319,21 +363,22 @@ async function exportItems(server, modVersions, items) {
|
||||
* Parses and merges all the locales for the all the mods given through
|
||||
* `modVersions` and `modOrder`.
|
||||
*
|
||||
* @param {module:host/src/server.FactorioServer} server -
|
||||
* The server to export the locale from.
|
||||
* @param {Map<string, string>} modVersions -
|
||||
* Mapping of mod name to version to export locale from.
|
||||
* @param {Array<string>} modOrder - Load order of the mods.
|
||||
* @param {string} languageCode - Language to export locale for.
|
||||
* @returns {Promise<Map<string, string>>} merged locale information
|
||||
* @memberof module:lib
|
||||
* @private
|
||||
* @inner
|
||||
* @param server - The server to export the locale from.
|
||||
* @param modVersions - Mapping of mod name to version to export locale from.
|
||||
* @param modOrder - Load order of the mods.
|
||||
* @param languageCode - Language to export locale for.
|
||||
* @returns merged locale information
|
||||
* @internal
|
||||
*/
|
||||
async function exportLocale(server, modVersions, modOrder, languageCode) {
|
||||
let mergedLocales = new Map();
|
||||
async function exportLocale(
|
||||
server: FactorioServer,
|
||||
modVersions: Map<string, string>,
|
||||
modOrder: string[],
|
||||
languageCode: string
|
||||
) {
|
||||
let mergedLocales = new Map<string, string>();
|
||||
|
||||
function mergeLocale(locale) {
|
||||
function mergeLocale(locale: Record<string, string | Record<string, string>>) {
|
||||
for (let [category, entries] of Object.entries(locale)) {
|
||||
if (typeof entries === "string") {
|
||||
mergedLocales.set(category, entries);
|
||||
@@ -357,7 +402,7 @@ async function exportLocale(server, modVersions, modOrder, languageCode) {
|
||||
let zip;
|
||||
try {
|
||||
zip = await loadZip(server, modVersions, mod);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
continue;
|
||||
}
|
||||
@@ -375,20 +420,18 @@ async function exportLocale(server, modVersions, modOrder, languageCode) {
|
||||
/**
|
||||
* Export the locale and item icons for the given factorio server
|
||||
*
|
||||
* @param {module:host/src/server.FactorioServer} server -
|
||||
* The server to export the data from.
|
||||
* @returns {Promise<JSZip>} zip file with exported data.
|
||||
* @memberof module:lib
|
||||
* @param server - The server to export the data from.
|
||||
* @returns zip file with exported data.
|
||||
*/
|
||||
async function exportData(server) {
|
||||
export async function exportData(server: FactorioServer) {
|
||||
await generateExportMod(server);
|
||||
|
||||
let settings = {};
|
||||
let prototypes = {};
|
||||
let settings: Prototypes = {};
|
||||
let prototypes: Prototypes = {};
|
||||
let modVersions = new Map();
|
||||
let modOrder = [];
|
||||
let modOrder: string[] = [];
|
||||
|
||||
function add(obj, prototype) {
|
||||
function add(obj: Prototypes, prototype: Prototype) {
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, prototype.type)) {
|
||||
obj[prototype.type] = {};
|
||||
}
|
||||
@@ -465,9 +508,5 @@ async function exportData(server) {
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
exportData,
|
||||
|
||||
// For testing only
|
||||
_exportLocale: exportLocale,
|
||||
};
|
||||
// For testing only
|
||||
export const _exportLocale = exportLocale;
|
||||
@@ -1,16 +1,36 @@
|
||||
// Library for patching Factorio saves with scenario code.
|
||||
|
||||
"use strict";
|
||||
const events = require("events");
|
||||
const fs = require("fs-extra");
|
||||
const JSZip = require("jszip");
|
||||
const path = require("path");
|
||||
const semver = require("semver");
|
||||
import events from "events";
|
||||
import fs from "fs-extra";
|
||||
import JSZip from "jszip";
|
||||
import path from "path";
|
||||
import semver from "semver";
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
|
||||
const knownScenarios = {
|
||||
interface ScenarioInfo {
|
||||
name: string;
|
||||
modules: string[];
|
||||
}
|
||||
|
||||
export interface ModuleInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
path: string;
|
||||
dependencies: Record<string, string>;
|
||||
load: string[];
|
||||
require: string[];
|
||||
}
|
||||
|
||||
interface PatchInfo {
|
||||
patch_number: number;
|
||||
scenario: ScenarioInfo;
|
||||
modules: { name: string, files: { path: string, load: boolean, require: boolean }[], }[];
|
||||
}
|
||||
|
||||
const knownScenarios: Record<string, ScenarioInfo> = {
|
||||
// First seen in 0.17.63
|
||||
"4e866186ebe297f1038fd325b09df1a1f5e2fdd1": {
|
||||
"name": "freeplay",
|
||||
@@ -24,13 +44,11 @@ const knownScenarios = {
|
||||
/**
|
||||
* Generates control.lua code for loading the Clusterio modules
|
||||
*
|
||||
* @param {Object} patchInfo - The patch info files's json content
|
||||
* @returns {string} Generated control.lua code.
|
||||
* @memberof module:lib
|
||||
* @private
|
||||
* @inner
|
||||
* @param patchInfo - The patch info files's json content
|
||||
* @returns Generated control.lua code.
|
||||
* @internal
|
||||
*/
|
||||
function generateLoader(patchInfo) {
|
||||
function generateLoader(patchInfo: PatchInfo) {
|
||||
let lines = [
|
||||
"-- Auto generated scenario module loader created by Clusterio",
|
||||
"-- Modifications to this file will be lost when loaded in Clusterio",
|
||||
@@ -75,12 +93,10 @@ function generateLoader(patchInfo) {
|
||||
* satisfy the property that dependencies are earlier in the array than
|
||||
* their dependents. Throws an error if this is not possible.
|
||||
*
|
||||
* @param {Array<Object>} modules - Array of modules to reorder
|
||||
* @memberof module:lib
|
||||
* @private
|
||||
* @inner
|
||||
* @param modules - Array of modules to reorder
|
||||
* @internal
|
||||
*/
|
||||
function reorderDependencies(modules) {
|
||||
function reorderDependencies(modules: ModuleInfo[]) {
|
||||
let index = 0;
|
||||
let present = new Map();
|
||||
let hold = new Map();
|
||||
@@ -141,7 +157,7 @@ function reorderDependencies(modules) {
|
||||
|
||||
// Start with a random module from the remaining modules
|
||||
for (let module of remaining.values()) {
|
||||
let cycle = [];
|
||||
let cycle: string[] = [];
|
||||
while (true) {
|
||||
// Find an unmet dependency
|
||||
let dependency = Object.keys(module.dependencies).find(name => !present.has(name));
|
||||
@@ -172,28 +188,33 @@ function reorderDependencies(modules) {
|
||||
* modules that were added. Will also remove any previous module
|
||||
* located in the save.
|
||||
*
|
||||
* @param {string} savePath - Path to the Factorio save to patch.
|
||||
* @param {Array<Object>} modules - Description of the modules to patch.
|
||||
* @memberof module:lib
|
||||
* @param savePath - Path to the Factorio save to patch.
|
||||
* @param modules - Description of the modules to patch.
|
||||
*/
|
||||
async function patch(savePath, modules) {
|
||||
export async function patch(savePath: string, modules: ModuleInfo[]) {
|
||||
let zip = await JSZip.loadAsync(await fs.readFile(savePath));
|
||||
let root = zip.folder(lib.findRoot(zip));
|
||||
let root = zip.folder(lib.findRoot(zip))!;
|
||||
|
||||
let patchInfoFile = root.file("clusterio.json");
|
||||
let patchInfo;
|
||||
let patchInfo: PatchInfo;
|
||||
if (patchInfoFile !== null) {
|
||||
let content = await patchInfoFile.async("string");
|
||||
patchInfo = JSON.parse(content);
|
||||
|
||||
// No info file present, try to detect if it's a known compatible scenario.
|
||||
} else {
|
||||
let controlStream = root.file("control.lua").nodeStream("nodebuffer");
|
||||
let controlFile = root.file("control.lua");
|
||||
if (!controlFile) {
|
||||
throw new Error("Unable to patch save, missing control.lua file.");
|
||||
}
|
||||
let controlStream = controlFile.nodeStream("nodebuffer");
|
||||
let controlHash = await lib.hashStream(controlStream);
|
||||
|
||||
if (controlHash in knownScenarios) {
|
||||
patchInfo = {
|
||||
"patch_number": 0,
|
||||
"scenario": knownScenarios[controlHash],
|
||||
"modules": [],
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Unable to patch save, unknown scenario (${controlHash})`);
|
||||
@@ -213,15 +234,15 @@ async function patch(savePath, modules) {
|
||||
|
||||
// Add the modules to the save.
|
||||
for (let module of modules) {
|
||||
let moduleDir = root.folder(`modules/${module.name}`);
|
||||
let moduleDir = root.folder(`modules/${module.name}`)!;
|
||||
let moduleInfo = {
|
||||
"name": module.name,
|
||||
"files": [],
|
||||
"files": [] as { path: string, load: boolean, require: boolean }[],
|
||||
};
|
||||
|
||||
let dirs = [[module.path, ""]];
|
||||
let dirs: [string, string][] = [[module.path, ""]];
|
||||
while (dirs.length) {
|
||||
let [dir, relativeDir] = dirs.pop();
|
||||
let [dir, relativeDir] = dirs.pop()!;
|
||||
for (let entry of await fs.readdir(dir, { withFileTypes: true })) {
|
||||
let fsPath = path.join(dir, entry.name);
|
||||
let relativePath = path.posix.join(relativeDir, entry.name);
|
||||
@@ -261,10 +282,6 @@ async function patch(savePath, modules) {
|
||||
await fs.rename(tempSavePath, savePath);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
patch,
|
||||
|
||||
// For testing only
|
||||
_generateLoader: generateLoader,
|
||||
_reorderDependencies: reorderDependencies,
|
||||
};
|
||||
// For testing only
|
||||
export const _generateLoader = generateLoader;
|
||||
export const _reorderDependencies = reorderDependencies;
|
||||
@@ -1,30 +1,27 @@
|
||||
// Factorio Server interface
|
||||
"use strict";
|
||||
const fs = require("fs-extra");
|
||||
const child_process = require("child_process");
|
||||
const path = require("path");
|
||||
const events = require("events");
|
||||
const util = require("util");
|
||||
const crypto = require("crypto");
|
||||
const rconClient = require("rcon-client");
|
||||
import fs from "fs-extra";
|
||||
import child_process from "child_process";
|
||||
import path from "path";
|
||||
import events from "events";
|
||||
import util from "util";
|
||||
import crypto from "crypto";
|
||||
import { Rcon } from "rcon-client";
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
|
||||
/**
|
||||
* Determines the version of Factorio the datadir is pointing to by
|
||||
* reading the changelog.txt in it.
|
||||
*
|
||||
* @param {string} changelogPath - Path to changelog.txt.
|
||||
* @memberof module:lib
|
||||
* @private
|
||||
* @inner
|
||||
* @param changelogPath - Path to changelog.txt.
|
||||
* @internal
|
||||
*/
|
||||
async function getVersion(changelogPath) {
|
||||
async function getVersion(changelogPath: string) {
|
||||
let changelog;
|
||||
try {
|
||||
changelog = await fs.readFile(changelogPath, "utf-8");
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
@@ -47,14 +44,12 @@ async function getVersion(changelogPath) {
|
||||
/**
|
||||
* Comparison function for sorting Factorio version strings
|
||||
*
|
||||
* @param {string} a - Version to compare.
|
||||
* @param {string} b - Version to compare.
|
||||
* @returns {number} 1 if a < b, 0 if a = b and -1 if a > b.
|
||||
* @memberof module:lib
|
||||
* @private
|
||||
* @inner
|
||||
* @param a - Version to compare.
|
||||
* @param b - Version to compare.
|
||||
* @returns 1 if a < b, 0 if a = b and -1 if a > b.
|
||||
* @internal
|
||||
*/
|
||||
function versionOrder(a, b) {
|
||||
function versionOrder(a: string, b: string) {
|
||||
let aParts = a.split(".").map(s => Number.parseInt(s, 10));
|
||||
let bParts = b.split(".").map(s => Number.parseInt(s, 10));
|
||||
|
||||
@@ -77,16 +72,14 @@ function versionOrder(a, b) {
|
||||
* Searches the given factorio dir for an installation of Factorio of
|
||||
* the target version.
|
||||
*
|
||||
* @param {string} factorioDir - Path to Factorio installation dir(s).
|
||||
* @param {string} targetVersion -
|
||||
* @param factorioDir - Path to Factorio installation dir(s).
|
||||
* @param targetVersion -
|
||||
* Version to look for, supports the special value "latest" for the
|
||||
* latest version available.
|
||||
* @returns {Promise<Array>} Array with path to data dir and version found.
|
||||
* @memberof module:lib
|
||||
* @private
|
||||
* @inner
|
||||
* @returns Array with path to data dir and version found.
|
||||
* @internal
|
||||
*/
|
||||
async function findVersion(factorioDir, targetVersion) {
|
||||
async function findVersion(factorioDir: string, targetVersion: string) {
|
||||
|
||||
// There are two supported setups: having the factorio dir be the actual
|
||||
// install directory, and having the factorio dir be a folder containing
|
||||
@@ -139,10 +132,8 @@ async function findVersion(factorioDir, targetVersion) {
|
||||
* Returns a random port number in the Dynamic Ports range as defined by
|
||||
* RFC 6335.
|
||||
*
|
||||
* @return {number} a number in the range 49152 to 65535.
|
||||
* @memberof module:lib
|
||||
* @private
|
||||
* @inner
|
||||
* @return a number in the range 49152 to 65535.
|
||||
* @internal
|
||||
*/
|
||||
function randomDynamicPort() {
|
||||
const start = 49152;
|
||||
@@ -157,17 +148,15 @@ function randomDynamicPort() {
|
||||
* Uses crypto.randomBytes to generate a secure alphanumeric password of
|
||||
* the given length.
|
||||
*
|
||||
* @param {number} length - the length of the password to generate.
|
||||
* @return {Promise<string>} password of the given length
|
||||
* @memberof module:lib
|
||||
* @private
|
||||
* @inner
|
||||
* @param length - the length of the password to generate.
|
||||
* @return password of the given length
|
||||
* @internal
|
||||
*/
|
||||
async function generatePassword(length) {
|
||||
function validChar(byte) {
|
||||
async function generatePassword(length: number) {
|
||||
function validChar(byte: number) {
|
||||
const ranges = ["az", "AZ", "09"];
|
||||
return ranges.some(range => (
|
||||
range.codePointAt(0) <= byte && byte <= range.codePointAt(1)
|
||||
range.codePointAt(0)! <= byte && byte <= range.codePointAt(1)!
|
||||
));
|
||||
}
|
||||
let randomBytesAsync = util.promisify(crypto.randomBytes);
|
||||
@@ -198,28 +187,22 @@ async function generatePassword(length) {
|
||||
*
|
||||
* TODO document output format.
|
||||
*
|
||||
* @param {string} line - A line of output not including the line terminator
|
||||
* @param {string} source - Passed into the output structure as source
|
||||
* @param line - A line of output not including the line terminator
|
||||
* @param source - Passed into the output structure as source
|
||||
*
|
||||
* @returns {object} - An object with interpeted data.
|
||||
* @memberof module:lib
|
||||
* @private
|
||||
* @inner
|
||||
* @returns - An object with interpeted data.
|
||||
* @internal
|
||||
*/
|
||||
function parseOutput(line, source) {
|
||||
let output = {
|
||||
source,
|
||||
};
|
||||
|
||||
function parseOutput(line: string, source: "stdout" | "stderr"): lib.ParsedFactorioOutput {
|
||||
// There are three broad categories of output from Factorio, the first kind
|
||||
// starts with the seconds since the server started and has a format of
|
||||
// " 0.704 message"
|
||||
const secRegex = /^ {0,3}(\d+\.\d+) (.*)$/;
|
||||
let secMatch = secRegex.exec(line);
|
||||
if (secMatch) {
|
||||
output.format = "seconds";
|
||||
output.time = secMatch[1];
|
||||
let secContent = secMatch[2];
|
||||
const format = "seconds";
|
||||
const time = secMatch[1];
|
||||
const secContent = secMatch[2];
|
||||
|
||||
// A seconds output has two different kinds of formats: The first is a
|
||||
// log level and source location and has a format of
|
||||
@@ -229,15 +212,25 @@ function parseOutput(line, source) {
|
||||
const secLogRegex = /^(\w+) ((\w+\.cpp:\d+)|([@=].*?:\d+)): (.*)$/;
|
||||
let secLogMatch = secLogRegex.exec(secContent);
|
||||
if (secLogMatch) {
|
||||
output.type = "log";
|
||||
output.level = secLogMatch[1];
|
||||
output.file = secLogMatch[2];
|
||||
output.message = secLogMatch[5];
|
||||
return {
|
||||
source,
|
||||
format,
|
||||
time,
|
||||
type: "log",
|
||||
level: secLogMatch[1],
|
||||
file: secLogMatch[2],
|
||||
message: secLogMatch[5],
|
||||
}
|
||||
|
||||
// The other possibility is that the content is a generic message
|
||||
} else {
|
||||
output.type = "generic";
|
||||
output.message = secContent;
|
||||
return {
|
||||
source,
|
||||
format,
|
||||
time,
|
||||
type: "generic",
|
||||
message: secContent,
|
||||
}
|
||||
}
|
||||
|
||||
// The second category of output starts with a date stamp of the format
|
||||
@@ -246,8 +239,8 @@ function parseOutput(line, source) {
|
||||
const dateRegex = /^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d) (.*)$/;
|
||||
let dateMatch = dateRegex.exec(line);
|
||||
if (dateMatch) {
|
||||
output.format = "date";
|
||||
output.time = dateMatch[1];
|
||||
const format = "date";
|
||||
const time = dateMatch[1];
|
||||
let dateContent = dateMatch[2];
|
||||
|
||||
// A date output has two general formats. The first is an action
|
||||
@@ -255,26 +248,47 @@ function parseOutput(line, source) {
|
||||
const dateActionRegex = /^\[(\w+)\] (.*)$/;
|
||||
let dateActionMatch = dateActionRegex.exec(dateContent);
|
||||
if (dateActionMatch) {
|
||||
output.type = "action";
|
||||
output.action = dateActionMatch[1];
|
||||
output.message = dateActionMatch[2];
|
||||
return {
|
||||
source,
|
||||
format,
|
||||
time,
|
||||
type: "action",
|
||||
action: dateActionMatch[1],
|
||||
message: dateActionMatch[2],
|
||||
}
|
||||
|
||||
// The other format is a generic message
|
||||
} else {
|
||||
output.type = "generic";
|
||||
output.message = dateContent;
|
||||
return {
|
||||
source,
|
||||
format,
|
||||
time,
|
||||
type: "generic",
|
||||
message: dateContent,
|
||||
}
|
||||
}
|
||||
|
||||
// The last category of output is simply a generic message with no
|
||||
// formating.
|
||||
} else {
|
||||
output.format = "none";
|
||||
output.type = "generic";
|
||||
output.message = line;
|
||||
return {
|
||||
source,
|
||||
format: "none",
|
||||
type: "generic",
|
||||
message: line,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
// https://stackoverflow.com/a/49402091
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
|
||||
interface Heuristic {
|
||||
filter: {
|
||||
[P in KeysOfUnion<lib.ParsedFactorioOutput>]?: string | string[] | RegExp;
|
||||
},
|
||||
action(this: FactorioServer, parsed: lib.ParsedFactorioOutput): void;
|
||||
}
|
||||
|
||||
// These are filters applied to the output from Factorio. The filter
|
||||
@@ -285,7 +299,7 @@ function parseOutput(line, source) {
|
||||
//
|
||||
// If the filter matches the action is called with the FactorioServer
|
||||
// instance bound as this.
|
||||
const outputHeuristics = [
|
||||
const outputHeuristics: Heuristic[] = [
|
||||
// Message indicating the RCON interface has started
|
||||
{
|
||||
filter: { type: "log", message: /^Starting RCON interface/ },
|
||||
@@ -309,7 +323,7 @@ const outputHeuristics = [
|
||||
message: /^Saving to _autosave\d+ \((non-)?blocking\)\.$/,
|
||||
},
|
||||
action: function(parsed) {
|
||||
let name = /^Saving to (_autosave\d+) /.exec(parsed.message)[1];
|
||||
let name = /^Saving to (_autosave\d+) /.exec(parsed.message)![1];
|
||||
this.emit("_autosave", name);
|
||||
},
|
||||
},
|
||||
@@ -354,7 +368,7 @@ const outputHeuristics = [
|
||||
// when an error occurs. Since we can't send input there we terminate the server
|
||||
// process when this happens.
|
||||
if (process.platform === "win32" && parsed.message === "Quitting: multiplayer error.") {
|
||||
this._server.kill();
|
||||
this._server!.kill();
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -389,6 +403,37 @@ const outputHeuristics = [
|
||||
];
|
||||
|
||||
|
||||
export interface FactorioServerOptions {
|
||||
/** Logger to use for reporting errors. */
|
||||
logger?: lib.Logger,
|
||||
/**
|
||||
* Version of Factorio to use. Can also be the string "latest" to use
|
||||
* the latest version found in `factorioDir`.
|
||||
*/
|
||||
version?: string,
|
||||
/** UDP port to host game on. */
|
||||
gamePort?: number,
|
||||
/** TCP port to use for RCON. */
|
||||
rconPort?: number,
|
||||
/** Password use for RCON. */
|
||||
rconPassword?: string,
|
||||
/** Turn on whitelisting. */
|
||||
enableWhitelist?: boolean,
|
||||
/** Enable Factorio.com based multiplayer bans. */
|
||||
enableAuthserverBans?: boolean,
|
||||
/** Enable verbose logging. */
|
||||
verboseLogging?: boolean,
|
||||
/** Strip paths in the console. */
|
||||
stripPaths?: boolean,
|
||||
/**
|
||||
* Maximum number of RCON commands transmitted in parallel on the RCON
|
||||
* connection.
|
||||
*/
|
||||
maxConcurrentCommands?: number
|
||||
/** Timeout in ms to wait for a response to a shutdown command. */
|
||||
hangTimeout?: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* Factorio Server interface
|
||||
*
|
||||
@@ -407,43 +452,60 @@ const outputHeuristics = [
|
||||
* - save-finished - invoked when the server has finished a manual save
|
||||
* - exit - invoked when the sterver has exited
|
||||
* @extends events.EventEmitter
|
||||
* @memberof module:lib
|
||||
*/
|
||||
class FactorioServer extends events.EventEmitter {
|
||||
export class FactorioServer extends events.EventEmitter {
|
||||
/** UDP port used for hosting the Factorio game server on */
|
||||
gamePort: number;
|
||||
/** TCP port used for RCON on the Factorio game server */
|
||||
rconPort: number;
|
||||
/** Password used for RCON on the Factorio game server */
|
||||
rconPassword: string;
|
||||
/** Enable player whitelist */
|
||||
enableWhitelist: boolean;
|
||||
/** Enable Factorio.com based multiplayer bans **/
|
||||
enableAuthserverBans: boolean;
|
||||
/** Enable verbose logging */
|
||||
verboseLogging: boolean
|
||||
/** Timeout in ms to wait for a response to a shutdown command */
|
||||
hangTimeout = 5000;
|
||||
|
||||
_factorioDir: string;
|
||||
_writeDir: string;
|
||||
|
||||
// Resolved in init
|
||||
_version: string | null = null;
|
||||
_dataDir: string | null = null;
|
||||
|
||||
|
||||
_logger: lib.Logger;
|
||||
_targetVersion: string;
|
||||
_state: "new" | "init" | "create" | "running" | "stopping" = "new";
|
||||
_server: child_process.ChildProcessWithoutNullStreams | null = null;
|
||||
_rconClient: Rcon | null= null;
|
||||
_rconReady = false;
|
||||
_gameReady = false;
|
||||
_stripRegExp?: RegExp;
|
||||
_maxConcurrentCommands = 5;
|
||||
|
||||
// Array of possible causes for an unexpected shutdown
|
||||
_unexpected: string[] = [];
|
||||
_killed = false;
|
||||
_runningAutosave: string | null = null;
|
||||
|
||||
|
||||
/**
|
||||
* Create a Factorio server interface
|
||||
*
|
||||
* @param {string} factorioDir - Directory of the Factorio install(s).
|
||||
* @param {string} writeDir - Directory to write runtime data to.
|
||||
* @param {object} options - Optional parameters.
|
||||
* @param {Logger} options.logger - Logger to use for reporting errors.
|
||||
* @param {string} options.version -
|
||||
* Version of Factorio to use. Can also be the string "latest" to
|
||||
* use the latest version found in `factorioDir`.
|
||||
* @param {number} options.gamePort - UDP port to host game on.
|
||||
* @param {number} options.rconPort - TCP port to use for RCON.
|
||||
* @param {string} options.rconPassword - Password use for RCON.
|
||||
* @param {boolean} options.enableWhitelist - Turn on whitelisting.
|
||||
* @param {boolean} options.enableAuthserverBans -
|
||||
* Enable Factorio.com based multiplayer bans.
|
||||
* @param {boolean} options.verboseLogging - Enable verbose logging.
|
||||
* @param {boolean} options.stripPaths - Strip paths in the console.
|
||||
* @param {number} options.maxConcurrentCommands -
|
||||
* Maximum number of RCON commands transmitted in parallel on the
|
||||
* RCON connection.
|
||||
* @param {number} options.hangTimeout -
|
||||
* Timeout in ms to wait for a response to a shutdown command.
|
||||
* @param factorioDir - Directory of the Factorio install(s).
|
||||
* @param writeDir - Directory to write runtime data to.
|
||||
* @param options - Optional parameters.
|
||||
*/
|
||||
constructor(factorioDir, writeDir, options) {
|
||||
constructor(factorioDir: string, writeDir: string, options: FactorioServerOptions) {
|
||||
super();
|
||||
|
||||
this._factorioDir = factorioDir;
|
||||
this._writeDir = writeDir;
|
||||
|
||||
// Resolved in init
|
||||
this._version = null;
|
||||
this._dataDir = null;
|
||||
|
||||
this._logger = options.logger || lib.logger;
|
||||
this._targetVersion = options.version || "latest";
|
||||
/** UDP port used for hosting the Factorio game server on */
|
||||
@@ -451,27 +513,22 @@ class FactorioServer extends events.EventEmitter {
|
||||
/** TCP port used for RCON on the Factorio game server */
|
||||
this.rconPort = options.rconPort || randomDynamicPort();
|
||||
/** Password used for RCON on the Factorio game server */
|
||||
this.rconPassword = options.rconPassword;
|
||||
this.rconPassword = options.rconPassword as string; // init will generate one if not there
|
||||
/** Enable player whitelist */
|
||||
this.enableWhitelist = options.enableWhitelist;
|
||||
this.enableWhitelist = options.enableWhitelist || false;
|
||||
/** Enable Factorio.com based multiplayer bans **/
|
||||
this.enableAuthserverBans = options.enableAuthserverBans;
|
||||
this.enableAuthserverBans = options.enableAuthserverBans || false;
|
||||
/** Enable verbose logging */
|
||||
this.verboseLogging = options.verboseLogging;
|
||||
this.verboseLogging = options.verboseLogging || false;
|
||||
/** Maximum number of RCON commands transmitted in parallel on the RCON connection */
|
||||
this.maxConcurrentCommands = options.maxConcurrentCommands || 5;
|
||||
/** Timeout in ms to wait for a response to a shutdown command */
|
||||
this.hangTimeout = options.hangTimeout || 5000;
|
||||
this._state = "new";
|
||||
this._server = null;
|
||||
this._rconClient = null;
|
||||
this._rconReady = false;
|
||||
this._gameReady = false;
|
||||
this.hangTimeout = options.hangTimeout ?? 5000;
|
||||
|
||||
if (options.stripPaths) {
|
||||
let chars = new Set(path.resolve(this.writePath("temp")));
|
||||
chars.delete(":"); // Having a colon could lead to matching the line number
|
||||
chars = [...chars].join("");
|
||||
let charSet = new Set(path.resolve(this.writePath("temp")));
|
||||
charSet.delete(":"); // Having a colon could lead to matching the line number
|
||||
let chars = [...charSet].join("");
|
||||
|
||||
let tempPath = `${path.resolve(this.writePath("temp", "currently-playing"))}${path.sep}`;
|
||||
let writePath = `${path.resolve(this.writePath())}${path.sep}`;
|
||||
@@ -485,12 +542,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
// Array of possible causes for an unexpected shutdown
|
||||
this._unexpected = [];
|
||||
this._killed = false;
|
||||
|
||||
// Track autosaving
|
||||
this._runningAutosave = null;
|
||||
this.on("_autosave", name => {
|
||||
this.emit("autosave-start", name);
|
||||
this._runningAutosave = name;
|
||||
@@ -506,7 +558,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
_check(expectedStates) {
|
||||
_check(expectedStates: FactorioServer["_state"][]) {
|
||||
if (!expectedStates.includes(this._state)) {
|
||||
throw new Error(
|
||||
`Expected state ${expectedStates} but state is ${this._state}`
|
||||
@@ -514,31 +566,31 @@ class FactorioServer extends events.EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async _handleIpc(line) {
|
||||
async _handleIpc(line: Buffer) {
|
||||
let channelEnd = line.indexOf("?");
|
||||
if (channelEnd === -1) {
|
||||
throw new Error(`Malformed IPC line "${line.toString()}"`);
|
||||
}
|
||||
|
||||
let channel = line
|
||||
.slice(6, channelEnd)
|
||||
.subarray(6, channelEnd)
|
||||
.toString("utf-8")
|
||||
.replace(/\\x([0-9a-f]{2})/g, (match, p1) => (
|
||||
String.fromCharCode(parseInt(p1, 16))
|
||||
))
|
||||
;
|
||||
|
||||
let type = line.slice(channelEnd + 1, channelEnd + 2).toString("utf-8");
|
||||
let type = line.subarray(channelEnd + 1, channelEnd + 2).toString("utf-8");
|
||||
let content;
|
||||
if (type === "j") {
|
||||
try {
|
||||
content = JSON.parse(line.slice(channelEnd + 2).toString("utf-8"));
|
||||
content = JSON.parse(line.subarray(channelEnd + 2).toString("utf-8"));
|
||||
} catch (err) {
|
||||
throw new Error(`Malformed JSON to ${channel}: ${line.slice(channelEnd + 2).toString("utf-8")}`);
|
||||
throw new Error(`Malformed JSON to ${channel}: ${line.subarray(channelEnd + 2).toString("utf-8")}`);
|
||||
}
|
||||
|
||||
} else if (type === "f") {
|
||||
let fileName = line.slice(channelEnd + 2).toString("utf-8");
|
||||
let fileName = line.subarray(channelEnd + 2).toString("utf-8");
|
||||
let filePath = this.writePath("script-output", fileName);
|
||||
|
||||
// Prevent malicious names
|
||||
@@ -564,15 +616,15 @@ class FactorioServer extends events.EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_handleOutput(line, source) {
|
||||
if (line.slice(0, 6).equals(Buffer.from("\f$ipc:"))) {
|
||||
this._handleIpc(line).catch(err => this.emit("error", err));
|
||||
_handleOutput(rawLine: Buffer, source: "stdout" | "stderr") {
|
||||
if (rawLine.subarray(0, 6).equals(Buffer.from("\f$ipc:"))) {
|
||||
this._handleIpc(rawLine).catch(err => this.emit("error", err));
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit(source, line);
|
||||
this.emit(source, rawLine);
|
||||
|
||||
line = line.toString("utf-8");
|
||||
let line = rawLine.toString("utf-8");
|
||||
if (this._stripRegExp) {
|
||||
line = line.replace(this._stripRegExp, "");
|
||||
}
|
||||
@@ -586,14 +638,14 @@ class FactorioServer extends events.EventEmitter {
|
||||
}
|
||||
|
||||
if (expected instanceof RegExp) {
|
||||
if (!expected.test(parsed[name])) {
|
||||
if (!expected.test((parsed as any)[name])) {
|
||||
continue heuristicLoop;
|
||||
}
|
||||
} else if (expected instanceof Array) {
|
||||
if (!expected.includes(parsed[name])) {
|
||||
if (!expected.includes((parsed as any)[name])) {
|
||||
continue heuristicLoop;
|
||||
}
|
||||
} else if (expected !== parsed[name]) {
|
||||
} else if (expected !== (parsed as any)[name]) {
|
||||
continue heuristicLoop;
|
||||
}
|
||||
}
|
||||
@@ -622,7 +674,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
maxPending: this.maxConcurrentCommands,
|
||||
};
|
||||
|
||||
this._rconClient = new rconClient.Rcon(config);
|
||||
this._rconClient = new Rcon(config);
|
||||
this._rconClient.on("error", () => { /* Ignore */ });
|
||||
this._rconClient.on("authenticated", () => { this._rconReady = true; this.emit("rcon-ready"); });
|
||||
this._rconClient.on("end", () => {
|
||||
@@ -635,15 +687,18 @@ class FactorioServer extends events.EventEmitter {
|
||||
await this._rconClient.connect();
|
||||
}
|
||||
|
||||
/** Maximum number of RCON commands transmitted in parallel on the RCON connection */
|
||||
get maxConcurrentCommands() {
|
||||
return this._maxConcurrentCommands;
|
||||
}
|
||||
|
||||
set maxConcurrentCommands(value) {
|
||||
set maxConcurrentCommands(value: number) {
|
||||
this._maxConcurrentCommands = value;
|
||||
this.setMaxListeners(value + 5);
|
||||
if (this._rconClient) {
|
||||
// @ts-ignore
|
||||
if (this._rconClient.sendQueue) {
|
||||
// @ts-ignore
|
||||
this._rconClient.sendQueue.maxConcurrent = value;
|
||||
}
|
||||
// XXX: Workaround to suppress bogus event listener warning
|
||||
@@ -662,7 +717,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
async init() {
|
||||
this._check(["new"]);
|
||||
[this._dataDir, this._version] = await findVersion(this._factorioDir, this._targetVersion);
|
||||
this.rconPassword = this.rconPassword || await generatePassword(10);
|
||||
this.rconPassword = this.rconPassword ?? await generatePassword(10);
|
||||
this._state = "init";
|
||||
}
|
||||
|
||||
@@ -672,7 +727,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
* constructor.
|
||||
*/
|
||||
get version() {
|
||||
return this._version;
|
||||
return this._version!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -688,10 +743,10 @@ class FactorioServer extends events.EventEmitter {
|
||||
_attachStdio() {
|
||||
let stdout = new lib.LineSplitter({ readableObjectMode: true });
|
||||
stdout.on("data", line => { this._handleOutput(line, "stdout"); });
|
||||
this._server.stdout.pipe(stdout);
|
||||
this._server!.stdout.pipe(stdout);
|
||||
let stderr = new lib.LineSplitter({ readableObjectMode: true });
|
||||
stderr.on("data", line => { this._handleOutput(line, "stderr"); });
|
||||
this._server.stderr.pipe(stderr);
|
||||
this._server!.stderr.pipe(stderr);
|
||||
}
|
||||
|
||||
_resetState() {
|
||||
@@ -705,7 +760,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
}
|
||||
|
||||
_watchExit() {
|
||||
this._server.on("exit", (code, signal) => {
|
||||
this._server!.on("exit", (code, signal) => {
|
||||
if (this._state !== "stopping") {
|
||||
if (signal === "SIGKILL") {
|
||||
if (this._killed) {
|
||||
@@ -744,7 +799,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
this._resetState();
|
||||
this.emit("exit");
|
||||
});
|
||||
this._server.on("error", err => {
|
||||
this._server!.on("error", (err: any) => {
|
||||
if (err.code === "EACCES") {
|
||||
this.emit("error", new lib.EnvironmentError("Unable to run server: Permission denied"));
|
||||
} else {
|
||||
@@ -777,15 +832,15 @@ class FactorioServer extends events.EventEmitter {
|
||||
* Spawns the Factorio server with the --create argument to create a new
|
||||
* map save with the given name.
|
||||
*
|
||||
* @param {string} name -
|
||||
* @param name -
|
||||
* Name of the save to create. Should end with ".zip".
|
||||
* @param {?number=} seed - Seed to pass via --map-gen-seed
|
||||
* @param {?object=} mapGenSettings -
|
||||
* @param seed - Seed to pass via --map-gen-seed
|
||||
* @param mapGenSettings -
|
||||
* Map get settings to pass via --map-gen-settings.
|
||||
* @param {?object=} mapSettings -
|
||||
* @param mapSettings -
|
||||
* Map setting to pass via --map-settings.
|
||||
*/
|
||||
async create(name, seed = null, mapGenSettings = null, mapSettings = null) {
|
||||
async create(name: string, seed?: number, mapGenSettings?: object, mapSettings?: object) {
|
||||
this._check(["init"]);
|
||||
this._state = "create";
|
||||
|
||||
@@ -796,11 +851,11 @@ class FactorioServer extends events.EventEmitter {
|
||||
[
|
||||
"--config", this.writePath("config.ini"),
|
||||
"--create", this.writePath("saves", name),
|
||||
/* eslint-disable eqeqeq, no-eq-null */
|
||||
...(seed != null ? ["--map-gen-seed", seed] : []),
|
||||
...(mapGenSettings != null ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : []),
|
||||
...(mapSettings != null ? ["--map-settings", this.writePath("map-settings.json")] : []),
|
||||
/* eslint-enable eqeqeq, no-eq-null */
|
||||
...(seed !== undefined ? ["--map-gen-seed", String(seed)] : []),
|
||||
...(mapGenSettings !== undefined ?
|
||||
["--map-gen-settings", this.writePath("map-gen-settings.json")] : []
|
||||
),
|
||||
...(mapSettings !== undefined ? ["--map-settings", this.writePath("map-settings.json")] : []),
|
||||
...(this.verboseLogging ? ["--verbose"] : []),
|
||||
],
|
||||
{
|
||||
@@ -818,7 +873,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
if (code !== 0) {
|
||||
throw new Error(`Factorio exited with status ${code}`);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "EACCES") {
|
||||
throw new lib.EnvironmentError("Unable to run server: Permission denied");
|
||||
}
|
||||
@@ -836,7 +891,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
*
|
||||
* @param {string} save - Name of the save to run.
|
||||
*/
|
||||
async start(save) {
|
||||
async start(save: string) {
|
||||
this._check(["init"]);
|
||||
this._state = "running";
|
||||
|
||||
@@ -846,8 +901,8 @@ class FactorioServer extends events.EventEmitter {
|
||||
[
|
||||
"--config", this.writePath("config.ini"),
|
||||
"--start-server", this.writePath("saves", save),
|
||||
"--port", this.gamePort,
|
||||
"--rcon-port", this.rconPort,
|
||||
"--port", String(this.gamePort),
|
||||
"--rcon-port", String(this.rconPort),
|
||||
"--rcon-password", this.rconPassword,
|
||||
...(this.enableWhitelist ? ["--use-server-whitelist"] : []),
|
||||
...(this.enableAuthserverBans ? ["--use-authserver-bans"] : []),
|
||||
@@ -870,14 +925,14 @@ class FactorioServer extends events.EventEmitter {
|
||||
* Spawn the Factorio server with the --start-server-load-scenario
|
||||
* argument to start the given scenario.
|
||||
*
|
||||
* @param {string} scenario - Name of the scenario to run.
|
||||
* @param {?number=} seed - Seed to pass via --map-gen-seed
|
||||
* @param {?object=} mapGenSettings -
|
||||
* @param scenario - Name of the scenario to run.
|
||||
* @param seed - Seed to pass via --map-gen-seed
|
||||
* @param mapGenSettings -
|
||||
* Map get settings to pass via --map-gen-settings.
|
||||
* @param {?object=} mapSettings -
|
||||
* @param mapSettings -
|
||||
* Map setting to pass via --map-settings.
|
||||
*/
|
||||
async startScenario(scenario, seed = null, mapGenSettings = null, mapSettings = null) {
|
||||
async startScenario(scenario: string, seed?: number, mapGenSettings?: object, mapSettings?: object) {
|
||||
this._check(["init"]);
|
||||
this._state = "running";
|
||||
|
||||
@@ -888,13 +943,13 @@ class FactorioServer extends events.EventEmitter {
|
||||
[
|
||||
"--config", this.writePath("config.ini"),
|
||||
"--start-server-load-scenario", scenario,
|
||||
/* eslint-disable eqeqeq, no-eq-null */
|
||||
...(seed != null ? ["--map-gen-seed", seed] : []),
|
||||
...(mapGenSettings != null ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : []),
|
||||
...(mapSettings != null ? ["--map-settings", this.writePath("map-settings.json")] : []),
|
||||
/* eslint-enable eqeqeq, no-eq-null */
|
||||
"--port", this.gamePort,
|
||||
"--rcon-port", this.rconPort,
|
||||
...(seed !== undefined ? ["--map-gen-seed", String(seed)] : []),
|
||||
...(mapGenSettings !== undefined ?
|
||||
["--map-gen-settings", this.writePath("map-gen-settings.json")] : []
|
||||
),
|
||||
...(mapSettings !== undefined ? ["--map-settings", this.writePath("map-settings.json")] : []),
|
||||
"--port", String(this.gamePort),
|
||||
"--rcon-port", String(this.rconPort),
|
||||
"--rcon-password", this.rconPassword,
|
||||
...(this.enableWhitelist ? ["--use-server-whitelist"] : []),
|
||||
...(this.enableAuthserverBans ? ["--use-authserver-bans"] : []),
|
||||
@@ -918,13 +973,13 @@ class FactorioServer extends events.EventEmitter {
|
||||
* If the rcon connection hasn't been established yet, this will
|
||||
* wait until it is establied and then send the message.
|
||||
*
|
||||
* @param {string} message - message to send to server over RCON.
|
||||
* @param {boolean} expectEmpty -
|
||||
* @param message - message to send to server over RCON.
|
||||
* @param expectEmpty -
|
||||
* if true throw if the response is not empty. Useful for detecting
|
||||
* errors that might have been sent in response.
|
||||
* @returns {Promise<string>} response from server.
|
||||
* @returns response from server.
|
||||
*/
|
||||
async sendRcon(message, expectEmpty) {
|
||||
async sendRcon(message: string, expectEmpty?: boolean) {
|
||||
this._check(["running", "stopping"]);
|
||||
if (!this._rconReady) {
|
||||
await events.once(this, "rcon-ready");
|
||||
@@ -959,13 +1014,13 @@ class FactorioServer extends events.EventEmitter {
|
||||
if (this._rconClient) {
|
||||
this._rconClient.end().catch(() => {});
|
||||
}
|
||||
this._server.kill("SIGKILL");
|
||||
this._server!.kill("SIGKILL");
|
||||
}
|
||||
}, this.hangTimeout);
|
||||
|
||||
// It's possible the server decides to exit on its own, in which
|
||||
// case the stop operation has to be cancelled.
|
||||
this._server.once("exit", () => {
|
||||
this._server!.once("exit", () => {
|
||||
stopped = true;
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
@@ -1011,7 +1066,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
// complaint for the lack of graceful shutdown to the Factorio
|
||||
// developers. Rcon does not recognize /quit, stdin is not
|
||||
// recognized, and there's no "send CTRL+C to process" on Windows.
|
||||
this._server.kill();
|
||||
this._server!.kill();
|
||||
|
||||
// There appears to be an race condition where sending SIGTERM
|
||||
// immediatly before the RCON interface comes online causes the
|
||||
@@ -1020,7 +1075,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
// case there's no recovering from it.
|
||||
if (process.platform !== "win32") {
|
||||
this.on("_quitting", setAlive);
|
||||
await events.once(this._server, "exit");
|
||||
await events.once(this._server!, "exit");
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
this.off("_quitting", setAlive);
|
||||
@@ -1028,7 +1083,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
// On windows the process is terminated immediately, but to keep
|
||||
// ordering wait until after the exit event here.
|
||||
} else {
|
||||
await events.once(this._server, "exit");
|
||||
await events.once(this._server!, "exit");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1036,7 +1091,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
* Kill the server
|
||||
*
|
||||
* Terminates the server without any cleanup or saving.
|
||||
* @param {boolean} [unexpected=false] -
|
||||
* @param unexpected -
|
||||
* If true raise an error event as a result of killing the Factorio
|
||||
* process.
|
||||
*/
|
||||
@@ -1046,8 +1101,8 @@ class FactorioServer extends events.EventEmitter {
|
||||
if (!unexpected) {
|
||||
this._state = "stopping";
|
||||
}
|
||||
this._server.kill("SIGKILL");
|
||||
await new Promise(resolve => this._server.once("exit", resolve));
|
||||
this._server!.kill("SIGKILL");
|
||||
await new Promise(resolve => this._server!.once("exit", resolve));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1056,7 +1111,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
* Ensures achievements are disabled on the save that's running. This is
|
||||
* necessary in order to run any commands at all.
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
* @returns
|
||||
* True if acheivements got disabled and false if they already where
|
||||
* disabled.
|
||||
*/
|
||||
@@ -1085,11 +1140,11 @@ class FactorioServer extends events.EventEmitter {
|
||||
* the data directory of the Factorio server. Not valid before init has
|
||||
* been called.
|
||||
*
|
||||
* @param {...string} parts - Extra parts to add to the data path.
|
||||
* @returns {string} Data directory path.
|
||||
* @param parts - Extra parts to add to the data path.
|
||||
* @returns Data directory path.
|
||||
*/
|
||||
dataPath(...parts) {
|
||||
return path.join(this._dataDir, ...parts);
|
||||
dataPath(...parts: string[]) {
|
||||
return path.join(this._dataDir!, ...parts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1097,7 +1152,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
*
|
||||
* Get the path to the factorio binary depending on the platform (MacOS support)
|
||||
*
|
||||
* @returns {string} Path to factorio binary
|
||||
* @returns Path to factorio binary
|
||||
*/
|
||||
binaryPath() {
|
||||
if (process.platform === "darwin") {
|
||||
@@ -1112,10 +1167,10 @@ class FactorioServer extends events.EventEmitter {
|
||||
* Creates a path using path.join with the given parts that's relative to
|
||||
* the write directory of the Factorio server.
|
||||
*
|
||||
* @param {...string} parts - Extra parts to add to the write path.
|
||||
* @returns {string} Write directory path.
|
||||
* @param parts - Extra parts to add to the write path.
|
||||
* @returns Write directory path.
|
||||
*/
|
||||
writePath(...parts) {
|
||||
writePath(...parts: string[]) {
|
||||
return path.join(this._writeDir, ...parts);
|
||||
}
|
||||
|
||||
@@ -1124,7 +1179,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
*
|
||||
* Loads server-settings.example.json from the data dir.
|
||||
*
|
||||
* @returns {Promise<object>} the parsed server-settings.
|
||||
* @returns the parsed server-settings.
|
||||
*/
|
||||
async exampleSettings() {
|
||||
return JSON.parse(await fs.readFile(this.dataPath("server-settings.example.json"), "utf-8"));
|
||||
@@ -1140,7 +1195,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
await lib.safeOutputFile(this.writePath("config.ini"), content);
|
||||
}
|
||||
|
||||
async _writeMapSettings(mapGenSettings, mapSettings) {
|
||||
async _writeMapSettings(mapGenSettings?: object, mapSettings?: object) {
|
||||
if (mapGenSettings) {
|
||||
await lib.safeOutputFile(
|
||||
this.writePath("map-gen-settings.json"), JSON.stringify(mapGenSettings, null, 4)
|
||||
@@ -1155,14 +1210,10 @@ class FactorioServer extends events.EventEmitter {
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
FactorioServer,
|
||||
|
||||
// For testing only
|
||||
_getVersion: getVersion,
|
||||
_versionOrder: versionOrder,
|
||||
_findVersion: findVersion,
|
||||
_randomDynamicPort: randomDynamicPort,
|
||||
_generatePassword: generatePassword,
|
||||
_parseOutput: parseOutput,
|
||||
};
|
||||
// For testing only
|
||||
export const _getVersion = getVersion;
|
||||
export const _versionOrder = versionOrder;
|
||||
export const _findVersion = findVersion;
|
||||
export const _randomDynamicPort = randomDynamicPort;
|
||||
export const _generatePassword = generatePassword;
|
||||
export const _parseOutput = parseOutput;
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"include": ["host.ts", "package.json", "src/**/*.ts", "src/**/*.js"],
|
||||
"compilerOptions": {
|
||||
"lib": ["es2023"],
|
||||
"module": "commonjs",
|
||||
"target": "es2022",
|
||||
"rootDir": ".",
|
||||
"outDir": "dist",
|
||||
"composite": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
@@ -439,17 +439,18 @@ export class InstanceConfig extends classes.Config {
|
||||
get(name: "factorio.rcon_port"): number | null;
|
||||
get(name: "factorio.rcon_password"): string | null;
|
||||
get(name: "factorio.player_online_autosave_slots"): number;
|
||||
get(name: "factorio.mod_pack"): string | null;
|
||||
get(name: "factorio.mod_pack"): number | null;
|
||||
get(name: "factorio.enable_save_patching"): boolean;
|
||||
get(name: "factorio.enable_whitelist"): boolean;
|
||||
get(name: "factorio.enable_authserver_bans"): boolean;
|
||||
get(name: "factorio.settings"): Object;
|
||||
get(name: "factorio.settings"): Record<string, unknown>;
|
||||
get(name: "factorio.verbose_logging"): boolean;
|
||||
get(name: "factorio.strip_paths"): boolean;
|
||||
get(name: "factorio.sync_adminlist"): boolean;
|
||||
get(name: "factorio.sync_whitelist"): boolean;
|
||||
get(name: "factorio.sync_banlist"): boolean;
|
||||
get(name: "factorio.max_concurrent_commands"): number;
|
||||
get(name: string) {
|
||||
get(name: string) {
|
||||
return super.get(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ export class Message {
|
||||
}
|
||||
}
|
||||
|
||||
export type MessageSrcDst = MessageRequest | MessageResponse | MessageResponseError | MessageEvent;
|
||||
export type MessageRoutable = MessageRequest | MessageResponse | MessageResponseError | MessageEvent;
|
||||
|
||||
export class HelloData {
|
||||
constructor(
|
||||
|
||||
@@ -81,12 +81,6 @@ export abstract class BaseConnector extends events.EventEmitter {
|
||||
}
|
||||
|
||||
type ConnectorState = "closed" | "connecting" | "connected" | "resuming";
|
||||
type ReliableMessages =
|
||||
| libData.MessageRequest
|
||||
| libData.MessageResponse
|
||||
| libData.MessageResponseError
|
||||
| libData.MessageEvent
|
||||
;
|
||||
|
||||
/**
|
||||
* Base connector for links
|
||||
@@ -102,7 +96,7 @@ export abstract class WebSocketBaseConnector extends BaseConnector {
|
||||
_heartbeatId: ReturnType<typeof setInterval> | null = null;
|
||||
_heartbeatInterval: number | null = null;
|
||||
_lastReceivedSeq = undefined;
|
||||
_sendBuffer: (ReliableMessages)[] = [];
|
||||
_sendBuffer: (libData.MessageRoutable)[] = [];
|
||||
|
||||
constructor(src: libData.Address, dst: libData.Address) {
|
||||
super(src, dst);
|
||||
@@ -249,7 +243,7 @@ export abstract class WebSocketBaseConnector extends BaseConnector {
|
||||
*
|
||||
* @param message - Message to send.
|
||||
*/
|
||||
send(message: ReliableMessages) {
|
||||
send(message: libData.MessageRoutable) {
|
||||
if (!["connected", "resuming"].includes(this._state)) {
|
||||
throw new libErrors.SessionLost("No session");
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { BaseConnector, WebSocketBaseConnector } from "./connectors";
|
||||
import { strict as assert } from "assert";
|
||||
import type { PluginInfo } from "../plugin";
|
||||
import type { User } from "../users";
|
||||
import type { AddressType, JSONDeserialisable, Message, MessageRequest, MessageEvent } from "../data";
|
||||
import type { AddressType, JSONDeserialisable, MessageRoutable, MessageRequest, MessageEvent } from "../data";
|
||||
|
||||
export interface Request<Req, Res> {
|
||||
constructor: Partial<JSONDeserialisable<Request<Req, Res>>> & {
|
||||
@@ -71,7 +71,7 @@ export type RequestHandler<Req, Res> = (request: Req, src: libData.Address, dst:
|
||||
export type EventHandler<T> = (event: T, src: libData.Address, dst: libData.Address) => Promise<void>;
|
||||
|
||||
interface Router {
|
||||
forwardMessage(origin: Link, message: Message, fallback: boolean): boolean,
|
||||
forwardMessage(origin: Link, message: MessageRoutable, fallback: boolean): boolean,
|
||||
}
|
||||
|
||||
// Some definitions for the terminology used here:
|
||||
@@ -166,7 +166,7 @@ export class Link {
|
||||
* @throws {libErrors.InvalidMessage} if the message is invalid or not handled.
|
||||
*/
|
||||
_processMessage(
|
||||
message: libData.MessageRequest | libData.MessageResponse | libData.MessageResponseError | libData.MessageEvent
|
||||
message: MessageRoutable
|
||||
) {
|
||||
if (!["request", "response", "responseError", "event"].includes(message.type)) {
|
||||
throw new libErrors.InvalidMessage(`Unhandled message type ${message.type}`);
|
||||
@@ -175,7 +175,10 @@ export class Link {
|
||||
let entry = this._validateMessage(message);
|
||||
if (entry && this.connector.dst.type === libData.Address.control) {
|
||||
try {
|
||||
this.validatePermission(message, entry); // Somewhat hacky, defined in ControlConnection
|
||||
this.validatePermission(
|
||||
message as libData.MessageRequest | libData.MessageEvent,
|
||||
entry
|
||||
); // Somewhat hacky, defined in ControlConnection
|
||||
} catch (err) {
|
||||
if (err instanceof libErrors.PermissionError) {
|
||||
return;
|
||||
@@ -229,10 +232,10 @@ export class Link {
|
||||
* @param message - Message to check.
|
||||
* @throws {libErrors.InvalidMessage} if the message is invalid.
|
||||
*/
|
||||
validateIngress(message: libData.Message) { }
|
||||
validateIngress(message: MessageRoutable) { }
|
||||
|
||||
_validateMessage(
|
||||
message: libData.MessageRequest | libData.MessageResponse | libData.MessageResponseError | libData.MessageEvent
|
||||
message: MessageRoutable
|
||||
) {
|
||||
try {
|
||||
if (message.src.type === libData.Address.broadcast) {
|
||||
@@ -307,9 +310,9 @@ export class Link {
|
||||
* @param entry - Request or Event entry for this Message.
|
||||
* @throws {libErrors.PermissionError} if unauthorized.
|
||||
*/
|
||||
validatePermission(message: libData.Message, entry: RequestEntry | EventEntry) { }
|
||||
validatePermission(message: libData.MessageRequest | libData.MessageEvent, entry: RequestEntry | EventEntry) { }
|
||||
|
||||
_routeMessage(message: libData.Message, entry?: RequestEntry | EventEntry) {
|
||||
_routeMessage(message: MessageRoutable, entry?: RequestEntry | EventEntry) {
|
||||
if (!this.router) {
|
||||
let err = new libErrors.InvalidMessage(
|
||||
`Received message addressed to ${(message as libData.MessageRequest).dst} but this link `+
|
||||
|
||||
@@ -165,7 +165,8 @@ export const logger = winston.createLogger({
|
||||
// into the type when you can define custom levels you want to support?
|
||||
export type Logger = Omit<winston.Logger,
|
||||
"error" | "warn" | "help" | "data" | "info" | "debug" | "prompt" | "http" | "verbose" | "input" | "silly" |
|
||||
"emerg" | "alert" | "crit" | "warning" | "notice"
|
||||
"emerg" | "alert" | "crit" | "warning" | "notice" |
|
||||
"child"
|
||||
> & {
|
||||
fatal: winston.LeveledLogMethod,
|
||||
error: winston.LeveledLogMethod,
|
||||
@@ -174,4 +175,5 @@ export type Logger = Omit<winston.Logger,
|
||||
info: winston.LeveledLogMethod,
|
||||
server: winston.LeveledLogMethod,
|
||||
verbose: winston.LeveledLogMethod,
|
||||
child(options: Object): Logger,
|
||||
};
|
||||
@@ -1696,28 +1696,23 @@ exposition.contentType = "text/plain; version=0.0.4";
|
||||
export function serializeResult(
|
||||
result: CollectorResult,
|
||||
options: {
|
||||
addLabels?: string,
|
||||
addLabels?: Record<string, string>,
|
||||
metricName?: string,
|
||||
metricHelp?: string,
|
||||
} = {}
|
||||
) {
|
||||
let addLabels = null;
|
||||
let metricName = result.metric.name;
|
||||
let metricHelp = result.metric.help;
|
||||
for (let [name, value] of Object.entries(options)) {
|
||||
if (name === "addLabels") {
|
||||
addLabels = value;
|
||||
} else if (name === "metricName") {
|
||||
metricName = value;
|
||||
} else if (name === "metricHelp") {
|
||||
metricHelp = value;
|
||||
} else {
|
||||
throw new Error(`Unrecognized option '${name}'`);
|
||||
}
|
||||
let {
|
||||
addLabels,
|
||||
metricName = result.metric.name,
|
||||
metricHelp = result.metric.help,
|
||||
...rest
|
||||
} = options;
|
||||
for (let name of Object.keys(rest)) {
|
||||
throw new Error(`Unrecognized option '${name}'`);
|
||||
}
|
||||
|
||||
let samples: Map<string, Map<string, number>>;
|
||||
if (addLabels === null) {
|
||||
if (!addLabels) {
|
||||
samples = result.samples;
|
||||
addLabels = {};
|
||||
|
||||
|
||||
+3
-2
@@ -3,7 +3,8 @@ const assert = require("assert").strict;
|
||||
const path = require("path");
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
const Host = require("@clusterio/host/src/Host");
|
||||
const Host = require("@clusterio/host/dist/src/Host").default;
|
||||
const { _discoverInstances } = require("@clusterio/host/dist/src/Host");
|
||||
|
||||
describe("Host testing", function() {
|
||||
before(function() {
|
||||
@@ -13,7 +14,7 @@ describe("Host testing", function() {
|
||||
describe("discoverInstances()", function() {
|
||||
it("should discover test instance", async function() {
|
||||
let instancePath = path.join("test", "file", "instances");
|
||||
let instanceInfos = await Host._discoverInstances(instancePath);
|
||||
let instanceInfos = await _discoverInstances(instancePath);
|
||||
|
||||
let referenceConfig = new lib.InstanceConfig("host");
|
||||
await referenceConfig.init();
|
||||
|
||||
@@ -4,7 +4,7 @@ const path = require("path");
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
const { PlayerStats, wait } = lib;
|
||||
const Instance = require("@clusterio/host/src/Instance");
|
||||
const Instance = require("@clusterio/host/dist/src/Instance").default;
|
||||
const { MockConnector, MockServer } = require("../mock");
|
||||
|
||||
const addr = lib.Address.fromShorthand;
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
const assert = require("assert").strict;
|
||||
const path = require("path");
|
||||
|
||||
const { FactorioServer } = require("@clusterio/host/src/server");
|
||||
const { _exportLocale } = require("@clusterio/host/src/export");
|
||||
const { FactorioServer } = require("@clusterio/host/dist/src/server");
|
||||
const { _exportLocale } = require("@clusterio/host/dist/src/export");
|
||||
|
||||
|
||||
describe("host/src/export", function() {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ const fs = require("fs-extra");
|
||||
const JSZip = require("jszip");
|
||||
const path = require("path");
|
||||
|
||||
const patch = require("@clusterio/host/src/patch");
|
||||
const patch = require("@clusterio/host/dist/src/patch");
|
||||
|
||||
|
||||
describe("host/patch", function() {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ const events = require("events");
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
|
||||
const hostServer = require("@clusterio/host/src/server");
|
||||
const hostServer = require("@clusterio/host/dist/src/server");
|
||||
const lib = require("@clusterio/lib");
|
||||
const { wait } = lib;
|
||||
const { testLines } = require("../lib/factorio/lines");
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
const assert = require("assert").strict;
|
||||
const path = require("path");
|
||||
|
||||
const { _exportLocale } = require("@clusterio/host/src/export");
|
||||
const { FactorioServer } = require("@clusterio/host/src/server");
|
||||
const { _exportLocale } = require("@clusterio/host/dist/src/export");
|
||||
const { FactorioServer } = require("@clusterio/host/dist/src/server");
|
||||
|
||||
|
||||
describe("Integration of lib/factorio/export", function() {
|
||||
|
||||
@@ -4,7 +4,7 @@ const fs = require("fs-extra");
|
||||
const JSZip = require("jszip");
|
||||
const path = require("path");
|
||||
|
||||
const { patch } = require("@clusterio/host/src/patch");
|
||||
const { patch } = require("@clusterio/host/dist/src/patch");
|
||||
|
||||
const { slowTest } = require("./index");
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
const { FactorioServer, _getVersion } = require("@clusterio/host/src/server");
|
||||
const { FactorioServer, _getVersion } = require("@clusterio/host/dist/src/server");
|
||||
const { logger } = lib;
|
||||
|
||||
const { slowTest } = require("./index");
|
||||
|
||||
Reference in new issue
Block a user