mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 07:04:53 +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.
186 lines
4.2 KiB
TypeScript
186 lines
4.2 KiB
TypeScript
import { logger } from "./logging.js";
|
|
import * as libHelpers from "./helpers.js";
|
|
|
|
export type HookHandler<Args extends unknown[], Result = void> = (...args: Args) => Result | Promise<Result>;
|
|
|
|
export class AsyncHook<
|
|
Args extends unknown[],
|
|
Result = void,
|
|
> {
|
|
private static readonly _timeoutToken = Symbol("timeout");
|
|
private _handlers = new Map<string, HookHandler<Args, Result>>();
|
|
|
|
constructor(
|
|
private _logger = logger,
|
|
private _timeoutMs = 15_000,
|
|
) { }
|
|
|
|
get size() {
|
|
return this._handlers.size;
|
|
}
|
|
|
|
get attached() {
|
|
return this._handlers.keys();
|
|
}
|
|
|
|
/**
|
|
* Stable event listener reference bound to this hook instance.
|
|
*
|
|
* This function is pre-bound and guaranteed to remain referentially stable
|
|
* for the lifetime of the hook instance. It can safely be passed directly
|
|
* into event emitters (e.g. EventEmitter.on / off) without losing context
|
|
* or requiring re-binding.
|
|
*
|
|
* Example:
|
|
* `eventEmitter.on("change", hook.listener);`
|
|
*/
|
|
readonly listener = this.invoke.bind(this);
|
|
|
|
/**
|
|
* Attach a handler to this hook.
|
|
*
|
|
* An error is thrown if another handler with the same name is already attached.
|
|
*/
|
|
attach(
|
|
name: string,
|
|
handler: HookHandler<Args, Result>,
|
|
) {
|
|
if (this._handlers.has(name)) {
|
|
throw new Error(`Handler with name ${name} is already attached`);
|
|
}
|
|
this._handlers.set(name, handler);
|
|
}
|
|
|
|
/**
|
|
* Remove a previously attached handler.
|
|
*
|
|
* If no handler exists, then no action is performed.
|
|
*/
|
|
detach(name: string) {
|
|
this._handlers.delete(name);
|
|
}
|
|
|
|
/**
|
|
* Remove all attached handlers.
|
|
*/
|
|
clear() {
|
|
this._handlers.clear();
|
|
}
|
|
|
|
private async _invokeHandler(
|
|
name: string,
|
|
handler: HookHandler<Args, Result>,
|
|
args: Args,
|
|
): Promise<Result | undefined> {
|
|
try {
|
|
const result = await libHelpers.timeout<
|
|
Result | typeof AsyncHook._timeoutToken
|
|
>(
|
|
Promise.resolve(handler(...args)),
|
|
this._timeoutMs,
|
|
AsyncHook._timeoutToken,
|
|
);
|
|
|
|
if (result === AsyncHook._timeoutToken) {
|
|
throw new Error(`Hook ${name} timed out after ${this._timeoutMs}ms`);
|
|
}
|
|
|
|
return result;
|
|
|
|
} catch (err: any) {
|
|
this._logger.error(`Ignoring error in hook "${name}":\n${err?.stack ?? err}`);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoke all handlers attached to this hook.
|
|
*
|
|
* Any return values are discarded.
|
|
*/
|
|
async invoke(...args: Args): Promise<void> {
|
|
await Promise.all(
|
|
[...this._handlers.entries()].map(
|
|
([name, handler]) => this._invokeHandler(name, handler, args)
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Invoke all handlers attached to this hook.
|
|
*
|
|
* Returns all successful results.
|
|
*/
|
|
async collect(...args: Args): Promise<Exclude<Awaited<Result>, void>[]> {
|
|
const results = await Promise.all(
|
|
[...this._handlers.entries()].map(
|
|
([name, handler]) => this._invokeHandler(name, handler, args)
|
|
)
|
|
);
|
|
|
|
return results.filter(
|
|
(result): result is Exclude<Awaited<Result>, void> => result !== undefined
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Invoke all handlers attached to this hook.
|
|
*
|
|
* Returns all successful results and their source.
|
|
*/
|
|
async collectEntries(...args: Args): Promise<[string, Result][]> {
|
|
const results = await Promise.all(
|
|
[...this._handlers.entries()].map(async ([name, handler]) => {
|
|
const result = await this._invokeHandler(name, handler, args);
|
|
return result !== undefined ? [name, result] as [string, Result] : undefined;
|
|
})
|
|
);
|
|
|
|
return results.filter(
|
|
(entry): entry is [string, Result] => entry !== undefined
|
|
);
|
|
}
|
|
}
|
|
|
|
export class AsyncHookCollection {
|
|
private hooks: AsyncHook<any[], any>[] = [];
|
|
|
|
constructor(
|
|
private _logger = logger,
|
|
) {}
|
|
|
|
protected newHook<
|
|
Args extends unknown[],
|
|
Result = void,
|
|
> (
|
|
_logger = this._logger,
|
|
timeoutMs?: number,
|
|
) {
|
|
const hook = new AsyncHook<Args, Result>(_logger, timeoutMs);
|
|
this.hooks.push(hook);
|
|
return hook;
|
|
}
|
|
|
|
get size() {
|
|
const attached = new Set(this.hooks.flatMap(hook => [...hook.attached]));
|
|
return attached.size;
|
|
}
|
|
|
|
get attached() {
|
|
const attached = new Set(this.hooks.flatMap(hook => [...hook.attached]));
|
|
return attached.values();
|
|
}
|
|
|
|
detachAll(name: string) {
|
|
for (const hook of this.hooks) {
|
|
hook.detach(name);
|
|
}
|
|
}
|
|
|
|
clearAll() {
|
|
for (const hook of this.hooks) {
|
|
hook.clear();
|
|
}
|
|
}
|
|
}
|