mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
The player proxy now listens on 34197 by default. New hosts default to the port range 34200-34299 so they don't hand that port to instances, and the controller logs an error and runs without the proxy if the port is taken, as hosts with the old range only skip it when the controller already holds it. Lobby instances start the save lobby.zip, created on first start as a one tile map with no autoplace and with the freeplay scenario replaced by an empty one. This uses the new chooseSave instance hook, which lets plugins pick or create the save to start, and setSaveScenario from @clusterio/host, which sets the scenario the save patcher keeps loading. Map gen settings can't use a width and height of 0 for this, as Factorio takes 0 to mean an unlimited map. The address players reach the proxy on moves from lobby.public_address to controller.player_proxy_public_address. When it's set the player_auth login page lists it once for the instances behind the proxy instead of their own addresses, and lobbies with player_auth loaded get a button that opens the web interface login dialog.
290 lines
6.8 KiB
JavaScript
290 lines
6.8 KiB
JavaScript
import events from "node:events";
|
|
import http from "node:http";
|
|
import express from "express";
|
|
|
|
import * as lib from "@clusterio/lib";
|
|
import { User, UserManager, UserRecord, InstanceManager, ControllerHooks } from "@clusterio/controller";
|
|
import { HostHooks, InstanceHooks } from "@clusterio/host";
|
|
|
|
const addr = lib.Address.fromShorthand;
|
|
|
|
export class MockLogger {
|
|
child() { return this; }
|
|
|
|
fatal() { }
|
|
|
|
error() { }
|
|
|
|
warn() { }
|
|
|
|
audit() { }
|
|
|
|
info() { }
|
|
|
|
server() { }
|
|
|
|
verbose() { }
|
|
}
|
|
|
|
export class MockSocket {
|
|
constructor() {
|
|
this.sentMessages = [];
|
|
this.events = new Map();
|
|
}
|
|
|
|
send(message) {
|
|
this.sentMessages.push(message);
|
|
}
|
|
|
|
on(event, fn) {
|
|
this.events.set(event, fn);
|
|
}
|
|
|
|
/* eslint-disable accessor-pairs */
|
|
set onclose(fn) {
|
|
this.on("close", (code, reason) => fn({ code, reason }));
|
|
}
|
|
|
|
set onerror(fn) {
|
|
this.on("error", fn);
|
|
}
|
|
|
|
set onopen(fn) {
|
|
this.on("open", fn);
|
|
}
|
|
|
|
set onmessage(fn) {
|
|
this.on("message", (data) => fn({ data }));
|
|
}
|
|
|
|
terminate() {
|
|
this.terminateCalled = true;
|
|
}
|
|
|
|
close() {
|
|
this.closeCalled = true;
|
|
}
|
|
}
|
|
|
|
export class MockConnector extends lib.BaseConnector {
|
|
constructor(src, dst) {
|
|
super(src, dst);
|
|
|
|
this.valid = true;
|
|
this.connected = true;
|
|
this.hasSession = true;
|
|
this.sentMessages = [];
|
|
}
|
|
|
|
send(message) {
|
|
this.sentMessages.push(message);
|
|
setImmediate(() => this.emit("send", message));
|
|
}
|
|
|
|
sendDisconnectReady() {
|
|
this.send(new lib.MessageDisconnect("ready"));
|
|
}
|
|
}
|
|
|
|
export class MockServer extends events.EventEmitter {
|
|
constructor() {
|
|
super();
|
|
this.reset();
|
|
}
|
|
|
|
reset() {
|
|
this.rconCommands = [];
|
|
this.rconCommandResults = new Map();
|
|
this.udpMessages = [];
|
|
}
|
|
|
|
async sendUdp(message) {
|
|
this.udpMessages.push(message);
|
|
}
|
|
|
|
handle(eventName, handler) {
|
|
this.on(`ipc-${eventName}`, event => handler(event).catch(err => {
|
|
this.emit("error", err);
|
|
}));
|
|
}
|
|
|
|
async sendRcon(command) {
|
|
this.rconCommands.push(command);
|
|
let result = this.rconCommandResults.get(command);
|
|
if (!result) {
|
|
result = { response: "" };
|
|
}
|
|
if (result instanceof Error || typeof result === "string") {
|
|
result = { response: result };
|
|
}
|
|
if (result.time) {
|
|
await lib.wait(result.time);
|
|
}
|
|
if (result.response instanceof Error) {
|
|
throw result.response;
|
|
}
|
|
return result.response || "";
|
|
}
|
|
}
|
|
|
|
export class MockInstance extends lib.Link {
|
|
constructor() {
|
|
super(new MockConnector(addr({ instanceId: 7357 }), addr({ hostId: 1 })));
|
|
this.logger = new MockLogger();
|
|
this.server = new MockServer();
|
|
this.name = "test";
|
|
this.id = 7357;
|
|
this.status = "running";
|
|
this.autoPaused = false;
|
|
this.hooks = new InstanceHooks(this.logger);
|
|
this.loadedPlugins = new Set();
|
|
this.mockConfigEntries = new Map([
|
|
["instance.id", 7357],
|
|
["factorio.enable_save_patching", true],
|
|
["factorio.enable_script_commands", true],
|
|
]);
|
|
this.config = {
|
|
get: (name) => {
|
|
if (this.mockConfigEntries.has(name)) {
|
|
return this.mockConfigEntries.get(name);
|
|
}
|
|
throw Error(`mock for field ${name} is not implemented`);
|
|
},
|
|
};
|
|
}
|
|
|
|
async sendRcon(command, expectEmpty, plugin) {
|
|
return await this.server.sendRcon(command);
|
|
}
|
|
|
|
async sendUdp(message) {
|
|
await this.server.sendUdp(message);
|
|
}
|
|
}
|
|
|
|
export class MockHost extends lib.Link {
|
|
constructor() {
|
|
super(new MockConnector(addr({ hostId: 1 }), addr("controller")));
|
|
this.hooks = new HostHooks(new MockLogger());
|
|
this.loadedPlugins = new Set();
|
|
}
|
|
}
|
|
|
|
export class MockControl extends lib.Link { }
|
|
|
|
export class MockController {
|
|
constructor() {
|
|
this.app = express();
|
|
this.app.locals.controller = this;
|
|
this.app.locals.streams = new Map();
|
|
this.mockConfigEntries = new Map([
|
|
["controller.public_url", "test"],
|
|
["controller.auth_secret", "TestSecretDoNotUse"],
|
|
["controller.proxy_stream_timeout", 1],
|
|
["controller.metrics_timeout", 1],
|
|
["controller.player_proxy_public_address", null],
|
|
]);
|
|
this.config = {
|
|
get: (name) => {
|
|
if (this.mockConfigEntries.has(name)) {
|
|
return this.mockConfigEntries.get(name);
|
|
}
|
|
throw Error(`mock for field ${name} is not implemented`);
|
|
},
|
|
};
|
|
|
|
const roleProvider = new lib.MemoryDatastoreProvider(new Map([
|
|
[0, lib.Role.fromJSON({ id: 0, name: "Admin", description: "admin", permissions: ["core.admin"] })],
|
|
[1, lib.Role.fromJSON({ id: 1, name: "Player", description: "player", permissions: [] })],
|
|
]));
|
|
|
|
this.roles = new lib.SubscribableDatastore(roleProvider, roleProvider.value);
|
|
this.roles.get(1).grantDefaultPermissions(); // Todo replace "core.admin" above with grantAdminPermissions
|
|
|
|
const userProvider = new lib.MemoryDatastoreProvider(new Map([
|
|
["test", UserRecord.fromJSON({ name: "test", roles: [0, 1] })],
|
|
["player", UserRecord.fromJSON({ name: "player", roles: [1] })],
|
|
]));
|
|
|
|
this.users = new UserManager(
|
|
new lib.SubscribableDatastore(userProvider, userProvider.value),
|
|
this.roles, this.config
|
|
);
|
|
|
|
this.instances = new InstanceManager(new lib.KeyValueDatastore(), this);
|
|
|
|
this.hosts = new lib.KeyValueDatastore();
|
|
this.handles = new Map();
|
|
this.hooks = new ControllerHooks(new MockLogger());
|
|
this.loadedPlugins = new Set();
|
|
this.wsServer = { hostConnections: new Map() };
|
|
}
|
|
|
|
get authSecret() {
|
|
return Buffer.from(this.config.get("controller.auth_secret"), "base64");
|
|
}
|
|
|
|
handle(eventClass, handler) {
|
|
this.handles.set(eventClass, handler);
|
|
}
|
|
|
|
deleteNote() { }
|
|
|
|
getControllerUrl() {
|
|
return "http://controller.example/";
|
|
}
|
|
|
|
async startServer() {
|
|
this.server = http.createServer(this.app);
|
|
await new Promise(resolve => {
|
|
this.server.listen(0, "127.0.0.1", resolve);
|
|
});
|
|
let address = this.server.address();
|
|
return `http://127.0.0.1:${address.port}`;
|
|
}
|
|
|
|
async stopServer() {
|
|
if (this.server) {
|
|
this.server.close();
|
|
this.server.unref();
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function createControllerPlugin(ControllerPluginClass, info) {
|
|
let controller = new MockController();
|
|
let metrics = {};
|
|
let logger = new MockLogger();
|
|
let plugin = new ControllerPluginClass(info, controller, metrics, logger);
|
|
await plugin.init();
|
|
return plugin;
|
|
}
|
|
|
|
export async function createInstancePlugin(InstancePluginClass, info) {
|
|
let instance = new MockInstance();
|
|
let host = new MockHost();
|
|
let plugin = new InstancePluginClass(info, instance, host, new MockLogger());
|
|
await plugin.init();
|
|
return plugin;
|
|
}
|
|
|
|
export async function loadControllerPlugin(entrypoint, info) {
|
|
const controller = new MockController();
|
|
await entrypoint({ controller, metrics: {}, logger: new MockLogger(), plugin: info });
|
|
return controller;
|
|
}
|
|
|
|
export async function loadInstancePlugin(entrypoint, info) {
|
|
const instance = new MockInstance();
|
|
const host = new MockHost();
|
|
await entrypoint({ instance, host, logger: new MockLogger(), plugin: info });
|
|
return { instance, host };
|
|
}
|
|
|
|
export function getHandler(link, Class) {
|
|
if (link instanceof MockController) {
|
|
return link.handles.get(Class);
|
|
}
|
|
return link._requestHandlers.get(Class) ?? link._eventHandlers.get(Class);
|
|
}
|