mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 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.
917 lines
30 KiB
TypeScript
917 lines
30 KiB
TypeScript
// Core definitions for the configuration system
|
|
import * as classes from "./classes.js";
|
|
import * as validators from "./validators.js";
|
|
import type { PluginNodeEnvInfo, PluginWebEnvInfo } from "../plugin.js";
|
|
import { Static } from "@sinclair/typebox";
|
|
import { isTargetVersion } from "../data/version.js";
|
|
import { SyncHook } from "../SyncHook.js";
|
|
|
|
type configFromJSON<T> = (...args: Parameters<typeof classes.Config.fromJSON>) => T;
|
|
type configFromFile<T> = (...args: Parameters<typeof classes.Config.fromFile>) => Promise<T>;
|
|
|
|
export interface ControllerConfigFields {
|
|
"controller.version": string;
|
|
"controller.name": string;
|
|
"controller.mods_directory": string;
|
|
"controller.database_directory": string;
|
|
"controller.http_port": number | null;
|
|
"controller.https_port": number | null;
|
|
"controller.bind_address": string | null;
|
|
"controller.trusted_proxies": string | null;
|
|
"controller.public_url": string | null;
|
|
"controller.static_url": string | null;
|
|
"controller.grafana_url": string | null;
|
|
"controller.tls_certificate": string | null;
|
|
"controller.tls_private_key": string | null;
|
|
"controller.auth_secret": string | null;
|
|
"controller.heartbeat_interval": number;
|
|
"controller.session_timeout": number;
|
|
"controller.max_send_buffer_size": number;
|
|
"controller.metrics_timeout": number;
|
|
"controller.max_subscription_filters": number;
|
|
"controller.system_metrics_interval": number;
|
|
"controller.proxy_stream_timeout": number;
|
|
"controller.player_proxy_port": number | null;
|
|
"controller.player_proxy_default_instance_id": number | null;
|
|
"controller.player_proxy_public_address": string | null;
|
|
"controller.factorio_username": string | null;
|
|
"controller.factorio_token": string | null;
|
|
"controller.share_factorio_credentials_with_hosts": boolean;
|
|
"controller.default_mod_pack_id": number | null;
|
|
"controller.default_role_id": number | null;
|
|
"controller.autosave_interval": number;
|
|
"controller.mod_portal_cache_duration_minutes": number;
|
|
"controller.mod_portal_page_size": number;
|
|
"controller.allow_remote_updates": boolean;
|
|
"controller.allow_plugin_updates": boolean;
|
|
"controller.allow_plugin_install": boolean;
|
|
[key: `${string}.load_plugin`]: boolean;
|
|
[key: `${string}.version`]: string;
|
|
}
|
|
|
|
/**
|
|
* Controller Config class
|
|
* @extends classes.Config
|
|
*/
|
|
export class ControllerConfig extends classes.Config<ControllerConfigFields> {
|
|
declare static fromJSON: configFromJSON<ControllerConfig>;
|
|
declare static fromFile: configFromFile<ControllerConfig>;
|
|
static pluginMigrations = new SyncHook<classes.ConfigSchema>();
|
|
static migrations(config: Static<typeof this.jsonSchema>) {
|
|
if (config.hasOwnProperty("controller.external_address")) {
|
|
config["controller.public_url"] = config["controller.external_address"];
|
|
delete config["controller.external_address"];
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
static fieldDefinitions: classes.ConfigDefs<ControllerConfigFields> = {
|
|
"controller.version": {
|
|
type: "string",
|
|
initialValue: "", // Set on start
|
|
readonly: ["controller"],
|
|
hidden: true,
|
|
},
|
|
"controller.name": {
|
|
title: "Name",
|
|
description: "Name of the cluster.",
|
|
type: "string",
|
|
initialValue: "Your Cluster",
|
|
},
|
|
"controller.mods_directory": {
|
|
title: "Mods Directory",
|
|
description: "Path to directory where mods shared with the cluster are stored.",
|
|
restartRequired: true,
|
|
type: "string",
|
|
initialValue: "mods",
|
|
validator: validators.filePath,
|
|
},
|
|
"controller.database_directory": {
|
|
title: "Database directory",
|
|
description: "Directory where item and configuration data is stored.",
|
|
type: "string",
|
|
initialValue: "database",
|
|
validator: validators.filePath,
|
|
},
|
|
"controller.autosave_interval": {
|
|
title: "Autosave Interval",
|
|
description: "Interval in seconds to autosave data in memory to disk.",
|
|
type: "number",
|
|
initialValue: 60,
|
|
validator: validators.greaterThanZero,
|
|
},
|
|
"controller.http_port": {
|
|
title: "HTTP Port",
|
|
description: "Port to listen for HTTP connections on, set to null to not listen for HTTP connections.",
|
|
restartRequired: true,
|
|
type: "number",
|
|
optional: true,
|
|
initialValue: 8080,
|
|
validator: validators.optional(validators.all(
|
|
validators.integer,
|
|
validators.greaterThanZero
|
|
)),
|
|
},
|
|
"controller.https_port": {
|
|
title: "HTTPS Port",
|
|
description: "Port to listen for HTTPS connection on, set to null to not listen for HTTPS connections.",
|
|
restartRequired: true,
|
|
type: "number",
|
|
optional: true,
|
|
validator: validators.optional(validators.all(
|
|
validators.integer,
|
|
validators.greaterThanZero
|
|
)),
|
|
},
|
|
"controller.bind_address": {
|
|
title: "Bind Address",
|
|
description: "IP address to bind the HTTP and HTTPS ports on.",
|
|
restartRequired: true,
|
|
type: "string",
|
|
optional: true,
|
|
// TODO extendedValidation, valid addresses including IPV4, IPV6, and localhost
|
|
},
|
|
"controller.trusted_proxies": {
|
|
title: "Trusted Proxies",
|
|
description:
|
|
"Comma separated list of IP addresses and/or CIDR blocks to trust the X-Forwarded-For header on",
|
|
type: "string",
|
|
optional: true,
|
|
// TODO extendedValidation, see parseTrustedProxies
|
|
},
|
|
"controller.public_url": {
|
|
title: "Public URL",
|
|
description: "Public facing URL the controller is hosted on, including the protocol.",
|
|
type: "string",
|
|
optional: true,
|
|
// TODO extendedValidation, valid url including the protocol
|
|
},
|
|
"controller.static_url": {
|
|
title: "Static URL",
|
|
description: "Override the URL to where static assets are hosted (must have a path consisting of /static/)",
|
|
type: "string",
|
|
optional: true,
|
|
validator: (value) => {
|
|
if (value) {
|
|
// This is unfortunately a limitiation of how the web build is set up right now.
|
|
const pathname = new URL(value, "http://localhost/").pathname;
|
|
if (pathname !== "/static/") {
|
|
throw new Error(`Path component of static_url must be /static/ not ${pathname}.`);
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"controller.grafana_url": {
|
|
title: "Grafana URL",
|
|
description: "URL of a Grafana dashboard to link from the overview page, including the protocol.",
|
|
type: "string",
|
|
optional: true,
|
|
},
|
|
"controller.tls_certificate": {
|
|
title: "TLS Certificate",
|
|
description: "Path to the certificate to use for HTTPS.",
|
|
restartRequired: true,
|
|
type: "string",
|
|
optional: true,
|
|
validator: validators.optional(validators.filePath),
|
|
},
|
|
"controller.tls_private_key": {
|
|
title: "TLS Private Key",
|
|
description: "Path to the private key to use for HTTPS.",
|
|
restartRequired: true,
|
|
type: "string",
|
|
optional: true,
|
|
validator: validators.optional(validators.filePath),
|
|
},
|
|
"controller.auth_secret": {
|
|
access: ["controller"],
|
|
title: "Controller Authentication Secret",
|
|
description:
|
|
"Secret used to generate and verify authentication tokens. " +
|
|
"Should be a long string of random letters and numbers. " +
|
|
"Do not share this.",
|
|
restartRequired: true,
|
|
type: "string",
|
|
optional: true,
|
|
},
|
|
"controller.heartbeat_interval": {
|
|
title: "Heartbeat Interval",
|
|
description: "Interval heartbeats are sent out on WebSocket connections.",
|
|
type: "number",
|
|
initialValue: 15,
|
|
validator: validators.greaterThanZero,
|
|
},
|
|
"controller.session_timeout": {
|
|
title: "Session Timeout",
|
|
description: "Time in seconds before giving up resuming a dropped WebSocket session.",
|
|
type: "number",
|
|
initialValue: 60,
|
|
validator: validators.greaterThanZero,
|
|
},
|
|
"controller.max_send_buffer_size": {
|
|
title: "Max Send Buffer Size",
|
|
description:
|
|
"MiB of unacknowledged messages to keep for a connection before ending its session, 0 for no limit.",
|
|
type: "number",
|
|
initialValue: 64,
|
|
validator: validators.greaterThanEqualZero,
|
|
},
|
|
"controller.metrics_timeout": {
|
|
title: "Metrics Timeout",
|
|
description: "Timeout in seconds for metrics gathering from hosts.",
|
|
type: "number",
|
|
initialValue: 8,
|
|
validator: validators.greaterThanZero,
|
|
},
|
|
"controller.max_subscription_filters": {
|
|
title: "Max Subscription Filters",
|
|
description: "Maximum number of filters a connection can hold for one subscribed event, 0 for no limit.",
|
|
type: "number",
|
|
initialValue: 10000,
|
|
restartRequired: true,
|
|
validator: validators.all(validators.integer, validators.greaterThanEqualZero),
|
|
},
|
|
"controller.system_metrics_interval": {
|
|
title: "System Metrics Interval",
|
|
description: "Interval in seconds to collect and update system metrics for the Web UI",
|
|
type: "number",
|
|
initialValue: 10,
|
|
validator: validators.greaterThanEqualZero,
|
|
},
|
|
"controller.proxy_stream_timeout": {
|
|
title: "Proxy Stream Timeout",
|
|
description: "Timeout in seconds for proxy streams to start flowing.",
|
|
type: "number",
|
|
initialValue: 15,
|
|
validator: validators.greaterThanZero,
|
|
},
|
|
"controller.player_proxy_port": {
|
|
title: "Player Proxy Port",
|
|
description:
|
|
"UDP port the controller accepts Factorio player connections on and forwards them to an " +
|
|
"instance, starting it if needed. Disabled when null.",
|
|
restartRequired: true,
|
|
type: "number",
|
|
optional: true,
|
|
initialValue: 34197,
|
|
validator: validators.optional(validators.greaterThanZero),
|
|
},
|
|
"controller.player_proxy_default_instance_id": {
|
|
title: "Player Proxy Default Instance",
|
|
description:
|
|
"Instance ID to send players connecting through the player proxy to when no plugin picks one " +
|
|
"and they have not been on any instance before.",
|
|
type: "number",
|
|
optional: true,
|
|
},
|
|
"controller.player_proxy_public_address": {
|
|
title: "Player Proxy Public Address",
|
|
description:
|
|
"Address players reach the player proxy on, for example play.example.com:34197. Used by plugins " +
|
|
"to tell players where to connect. If null plugins may assume the host name of " +
|
|
"controller.public_url and controller.player_proxy_port.",
|
|
type: "string",
|
|
optional: true,
|
|
},
|
|
"controller.factorio_username": {
|
|
title: "Factorio Username",
|
|
description: "Username to authenticate with Factorio API with.",
|
|
autoComplete: "section-factorio username",
|
|
type: "string",
|
|
optional: true,
|
|
},
|
|
"controller.factorio_token": {
|
|
title: "Factorio Token",
|
|
description: "Token to authenticate with Factorio API with.",
|
|
autoComplete: "section-factorio new-password",
|
|
type: "string",
|
|
credential: ["controller"],
|
|
optional: true,
|
|
},
|
|
"controller.share_factorio_credentials_with_hosts": {
|
|
title: "Share Factorio credentials with Hosts",
|
|
description:
|
|
"If enabled, the Factorio Username and Token will be shared with hosts and " +
|
|
"used in the server settings of instances in this cluster.",
|
|
type: "boolean",
|
|
initialValue: true,
|
|
},
|
|
"controller.default_mod_pack_id": {
|
|
title: "Default Mod Pack",
|
|
description: "Mod pack used by default for instances.",
|
|
inputComponent: "mod_pack",
|
|
type: "number",
|
|
optional: true,
|
|
},
|
|
"controller.default_role_id": {
|
|
title: "Default role",
|
|
description: "ID of role assigned by default to new users.",
|
|
inputComponent: "role",
|
|
type: "number",
|
|
optional: true,
|
|
initialValue: 1,
|
|
},
|
|
"controller.mod_portal_cache_duration_minutes": {
|
|
title: "Mod Portal Cache Duration",
|
|
description: "Duration in minutes to cache mod portal API responses.",
|
|
type: "number",
|
|
initialValue: 30,
|
|
validator: validators.greaterThanEqualZero, // Zero is allowed here to disable cache
|
|
},
|
|
"controller.mod_portal_page_size": {
|
|
title: "Mod Portal Page Size",
|
|
description: "Maximum number of results per page when querying the Factorio mod portal API.",
|
|
type: "number",
|
|
initialValue: 1000,
|
|
validator: validators.greaterThanZero,
|
|
},
|
|
"controller.allow_remote_updates": {
|
|
description: "When true, allows a remote event to trigger a clusterio update via npm",
|
|
type: "boolean",
|
|
initialValue: true,
|
|
readonly: ["controller"],
|
|
hidden: true,
|
|
},
|
|
"controller.allow_plugin_updates": {
|
|
description: "When true, allows a remote event to trigger a plugin update via npm",
|
|
type: "boolean",
|
|
initialValue: true,
|
|
readonly: ["controller"],
|
|
hidden: true,
|
|
},
|
|
"controller.allow_plugin_install": {
|
|
description: "When true, allows a remote event to trigger a plugin install via npm",
|
|
type: "boolean",
|
|
initialValue: false,
|
|
readonly: ["controller"],
|
|
hidden: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
export interface HostConfigFields {
|
|
"host.version": string;
|
|
"host.name": string;
|
|
"host.id": number;
|
|
"host.factorio_directory": string;
|
|
"host.mods_directory": string;
|
|
"host.instances_directory": string;
|
|
"host.controller_url": string;
|
|
"host.controller_token": string;
|
|
"host.public_address": string;
|
|
"host.factorio_port_range": string;
|
|
"host.factorio_username": string | null,
|
|
"host.factorio_token": string | null,
|
|
"host.max_reconnect_delay": number;
|
|
"host.allow_remote_updates": boolean;
|
|
"host.allow_plugin_updates": boolean;
|
|
"host.allow_plugin_install": boolean;
|
|
[key: `${string}.load_plugin`]: boolean;
|
|
[key: `${string}.version`]: string;
|
|
}
|
|
|
|
/**
|
|
* Host Config class
|
|
* @extends classes.Config
|
|
*/
|
|
export class HostConfig extends classes.Config<HostConfigFields> {
|
|
declare static fromJSON: configFromJSON<HostConfig>;
|
|
declare static fromFile: configFromFile<HostConfig>;
|
|
static pluginMigrations = new SyncHook<classes.ConfigSchema>();
|
|
static fieldDefinitions: classes.ConfigDefs<HostConfigFields> = {
|
|
"host.version": {
|
|
type: "string",
|
|
initialValue: "", // Set on start
|
|
readonly: ["host"],
|
|
hidden: true,
|
|
},
|
|
"host.name": {
|
|
description: "Name of the host",
|
|
type: "string",
|
|
initialValue: "New Host",
|
|
},
|
|
"host.id": {
|
|
description: "ID of the host",
|
|
type: "number",
|
|
initialValue: () => Math.random() * 2**31 | 0,
|
|
hidden: true,
|
|
},
|
|
"host.factorio_directory": {
|
|
description: "Path to directory to look for factorio installs",
|
|
type: "string",
|
|
initialValue: "factorio",
|
|
validator: validators.filePath,
|
|
},
|
|
"host.mods_directory": {
|
|
title: "Mods Directory",
|
|
description: "Path to directory where mods for instances are cached.",
|
|
restartRequired: true,
|
|
type: "string",
|
|
initialValue: "mods",
|
|
validator: validators.filePath,
|
|
},
|
|
"host.instances_directory": {
|
|
description: "Path to directory to store instances in.",
|
|
restartRequired: true,
|
|
type: "string",
|
|
initialValue: "instances",
|
|
validator: validators.filePath,
|
|
},
|
|
"host.controller_url": {
|
|
description: "URL to connect to the controller at",
|
|
restartRequired: true,
|
|
type: "string",
|
|
initialValue: "http://localhost:8080/",
|
|
// TODO extendedValidation, valid url including the protocol
|
|
},
|
|
"host.controller_token": {
|
|
access: ["host"],
|
|
description: "Token to authenticate to controller with.",
|
|
restartRequired: true,
|
|
type: "string",
|
|
initialValue: "enter token here",
|
|
// TODO extendedValidation, valid JWT with matching host id
|
|
},
|
|
"host.public_address": {
|
|
description: "Public facing address players should connect to in order to join instances on this host",
|
|
type: "string",
|
|
initialValue: "localhost",
|
|
},
|
|
"host.factorio_port_range": {
|
|
title: "Factorio port range",
|
|
description:
|
|
"Range of UDP ports to use for game connections. Supports both comma separated values and " +
|
|
"ranges separated with a dash.",
|
|
type: "string",
|
|
initialValue: "34200-34299",
|
|
// TODO extendedValidation, port range
|
|
},
|
|
"host.factorio_username": {
|
|
title: "Factorio Username",
|
|
description:
|
|
"Username to authenticate with Factorio API with. If set this will be used in the server settings " +
|
|
"of all instances on this host.",
|
|
autoComplete: "section-factorio username",
|
|
type: "string",
|
|
optional: true,
|
|
},
|
|
"host.factorio_token": {
|
|
title: "Factorio Token",
|
|
description:
|
|
"Token to authenticate with Factorio API with. If set this will be used in the server settings " +
|
|
"of all instances on this host.",
|
|
autoComplete: "section-factorio new-password",
|
|
type: "string",
|
|
credential: ["host"],
|
|
optional: true,
|
|
},
|
|
"host.max_reconnect_delay": {
|
|
title: "Max Reconnect Delay",
|
|
description: "Maximum delay to wait before attempting to reconnect WebSocket",
|
|
type: "number",
|
|
initialValue: 60,
|
|
validator: validators.greaterThanZero,
|
|
},
|
|
"host.allow_remote_updates": {
|
|
description: "When true, allows a remote event to trigger a clusterio update via npm",
|
|
type: "boolean",
|
|
initialValue: true,
|
|
readonly: ["host"],
|
|
hidden: true,
|
|
},
|
|
"host.allow_plugin_updates": {
|
|
description: "When true, allows a remote event to trigger a plugin update via npm",
|
|
type: "boolean",
|
|
initialValue: true,
|
|
readonly: ["host"],
|
|
hidden: true,
|
|
},
|
|
"host.allow_plugin_install": {
|
|
description: "When true, allows a remote event to trigger a plugin install via npm",
|
|
type: "boolean",
|
|
initialValue: false,
|
|
readonly: ["host"],
|
|
hidden: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
export interface InstanceConfigFields {
|
|
"instance.name": string;
|
|
"instance.id": number;
|
|
"instance.assigned_host": number | null;
|
|
"instance.auto_start": boolean;
|
|
"instance.player_proxy_port": number | null;
|
|
"instance.exclude_from_start_all": boolean;
|
|
[key: `${string}.load_plugin`]: boolean;
|
|
[key: `${string}.version`]: string;
|
|
|
|
"factorio.version": string;
|
|
"factorio.executable_path": string | null;
|
|
"factorio.shutdown_timeout": number;
|
|
"factorio.game_port": number | null;
|
|
"factorio.host_assigned_game_port": number | null;
|
|
"factorio.rcon_port": number | null;
|
|
"factorio.rcon_password": string | null;
|
|
"factorio.enable_lua_udp": boolean;
|
|
"factorio.lua_udp_port": number | null;
|
|
"factorio.player_online_autosave_slots": number;
|
|
"factorio.mod_pack_id": number | null;
|
|
"factorio.enable_save_patching": boolean;
|
|
"factorio.enable_script_commands": boolean;
|
|
"factorio.enable_whitelist": boolean;
|
|
"factorio.enable_authserver_bans": boolean;
|
|
"factorio.settings": Record<string, unknown>;
|
|
"factorio.verbose_logging": boolean;
|
|
"factorio.console_logging": boolean;
|
|
"factorio.strip_paths": boolean;
|
|
"factorio.sync_adminlist": "enabled" | "disabled" | "bidirectional";
|
|
"factorio.sync_whitelist": "enabled" | "disabled" | "bidirectional";
|
|
"factorio.sync_banlist": "enabled" | "disabled" | "bidirectional";
|
|
"factorio.max_concurrent_commands": number;
|
|
}
|
|
|
|
|
|
/**
|
|
* Instance config class
|
|
* @extends classes.Config
|
|
*/
|
|
export class InstanceConfig extends classes.Config<InstanceConfigFields> {
|
|
declare static fromJSON: configFromJSON<InstanceConfig>;
|
|
declare static fromFile: configFromFile<InstanceConfig>;
|
|
static pluginMigrations = new SyncHook<classes.ConfigSchema>();
|
|
static migrations(config: Static<typeof this.jsonSchema>) {
|
|
function boolToEnableDisable(name: string) {
|
|
if (config.hasOwnProperty(name) && typeof config[name] === "boolean") {
|
|
config[name] = config[name] ? "enabled" : "disabled";
|
|
}
|
|
}
|
|
|
|
boolToEnableDisable("factorio.sync_adminlist");
|
|
boolToEnableDisable("factorio.sync_whitelist");
|
|
boolToEnableDisable("factorio.sync_banlist");
|
|
|
|
return config;
|
|
}
|
|
|
|
static fieldDefinitions: classes.ConfigDefs<InstanceConfigFields> = {
|
|
"instance.name": {
|
|
type: "string",
|
|
initialValue: "New Instance",
|
|
},
|
|
"instance.id": {
|
|
description: "ID of the instance",
|
|
type: "number",
|
|
initialValue: () => Math.random() * 2**31 | 0,
|
|
hidden: true,
|
|
},
|
|
"instance.assigned_host": {
|
|
type: "number",
|
|
optional: true,
|
|
hidden: true,
|
|
// Internal value, extendedValidation not used
|
|
},
|
|
"instance.auto_start": {
|
|
description: "Automatically start this instance when the host hosting it is started up",
|
|
type: "boolean",
|
|
initialValue: false,
|
|
},
|
|
"instance.player_proxy_port": {
|
|
description:
|
|
"Extra UDP port on the controller's player proxy that sends players straight to this instance, " +
|
|
"starting it if needed. Requires controller.player_proxy_port to be set.",
|
|
type: "number",
|
|
optional: true,
|
|
validator: validators.optional(validators.greaterThanZero),
|
|
},
|
|
"instance.exclude_from_start_all": {
|
|
description: "Exclude this instance from the 'Start all' button operation",
|
|
type: "boolean",
|
|
initialValue: false,
|
|
},
|
|
"factorio.version": {
|
|
description:
|
|
"Version of the game to run. Use 'latest' for the latest installed version, a release " +
|
|
"channel such as 'stable' or 'experimental', or a specific X.Y or X.Y.Z version.",
|
|
restartRequired: true,
|
|
type: "string",
|
|
initialValue: "latest",
|
|
inputComponent: "target_version",
|
|
validator: function(value) {
|
|
if (!isTargetVersion(value)) {
|
|
throw new Error("Value must be 'latest', a release channel, or match X.Y or X.Y.Z");
|
|
}
|
|
},
|
|
},
|
|
"factorio.executable_path": {
|
|
description:
|
|
"Relative path from the Factorio installation directory to the executable to run. " +
|
|
"Defaults to auto detect the path, only needed in special setups.",
|
|
restartRequired: true,
|
|
type: "string",
|
|
optional: true,
|
|
validator: validators.optional(validators.filePath),
|
|
},
|
|
"factorio.shutdown_timeout": {
|
|
description:
|
|
"Timeout in seconds to wait after requesting the server to stop before killing " +
|
|
"the process. Set to 0 to disable.",
|
|
type: "number",
|
|
initialValue: 300,
|
|
validator: validators.greaterThanEqualZero,
|
|
},
|
|
"factorio.game_port": {
|
|
description: "UDP port to run game on, uses a port in host.factorio_port_range if null",
|
|
restartRequired: true,
|
|
type: "number",
|
|
optional: true,
|
|
validator: validators.optional(validators.greaterThanEqualZero),
|
|
},
|
|
"factorio.host_assigned_game_port": {
|
|
access: ["host"],
|
|
type: "number",
|
|
optional: true,
|
|
// Internal value, extendedValidation not used
|
|
},
|
|
"factorio.rcon_port": {
|
|
description: "TCP port to run RCON on, uses a random port if null",
|
|
restartRequired: true,
|
|
type: "number",
|
|
optional: true,
|
|
validator: validators.optional(validators.greaterThanEqualZero),
|
|
},
|
|
"factorio.rcon_password": {
|
|
credential: ["host", "controller"],
|
|
description: "Password for RCON, randomly generated if null.",
|
|
restartRequired: true,
|
|
type: "string",
|
|
optional: true,
|
|
},
|
|
"factorio.enable_lua_udp": {
|
|
description:
|
|
"Enable UDP messaging between the Factorio server and the host. " +
|
|
"Required by plugins using UDP. Requires Factorio 2.1.10 or later.",
|
|
restartRequired: true, // Because of the cli option "--enable-lua-udp"
|
|
type: "boolean",
|
|
initialValue: false,
|
|
},
|
|
"factorio.lua_udp_port": {
|
|
description: "UDP port for the Factorio server to receive Lua UDP messages on, uses a random port if null",
|
|
restartRequired: true,
|
|
type: "number",
|
|
optional: true,
|
|
validator: validators.optional(validators.greaterThanEqualZero),
|
|
},
|
|
"factorio.player_online_autosave_slots": {
|
|
description:
|
|
"Rename autosaves where players have been online since the previous autosave into a separate " +
|
|
"autosave pool with this many slots. Requires autosaves to be enabled to work. Set to 0 to disable.",
|
|
type: "number",
|
|
initialValue: 5,
|
|
dependsOn: ["factorio.settings", "factorio.settings.autosave_slots"],
|
|
validator: function(value, config) {
|
|
const factorioSettings = config.get("factorio.settings");
|
|
const autosaveSlots = factorioSettings.autosave_slots;
|
|
if (value !== 0 && typeof autosaveSlots === "number" && value < autosaveSlots) {
|
|
throw new Error("Value cannot be less than the number of autosave slots");
|
|
}
|
|
},
|
|
},
|
|
"factorio.mod_pack_id": {
|
|
title: "Mod Pack",
|
|
description:
|
|
"Mod pack to use on this server, if not set the default configured on the controller will be used",
|
|
inputComponent: "mod_pack",
|
|
restartRequired: true,
|
|
type: "number",
|
|
optional: true,
|
|
},
|
|
"factorio.enable_save_patching": {
|
|
description:
|
|
"Patch saves with Lua code. Required for Clusterio integrations, lua modules, and most plugins.",
|
|
restartRequired: true,
|
|
type: "boolean",
|
|
initialValue: true,
|
|
},
|
|
"factorio.enable_script_commands": {
|
|
description:
|
|
"Allows achievement breaking commands to be executed over rcon. " +
|
|
"Required for Clusterio integrations and most plugins. " +
|
|
"This does not prevent players using script commands.",
|
|
type: "boolean",
|
|
initialValue: true,
|
|
},
|
|
"factorio.enable_whitelist": {
|
|
description: "Turn on whitelist for joining the server.",
|
|
restartRequired: true, // Because of the cli option "--use-server-whitelist"
|
|
type: "boolean",
|
|
initialValue: false,
|
|
},
|
|
"factorio.enable_authserver_bans": {
|
|
description: "Turn on Factorio.com based multiplayer bans.",
|
|
restartRequired: true, // Because of the cli option "--use-authserver-bans"
|
|
type: "boolean",
|
|
initialValue: false,
|
|
},
|
|
"factorio.settings": {
|
|
description: "Settings overridden in server-settings.json",
|
|
restartRequired: true,
|
|
restartRequiredProps: [
|
|
"afk_autokick_interval", "allow_commands", "autosave_interval", "autosave_only_on_server",
|
|
"description", "ignore_player_limit_for_returning_players", "max_players", "max_upload_slots",
|
|
"max_upload_in_kilobytes_per_second", "name", "only_admins_can_pause_the_game", "game_password",
|
|
"require_user_verification", "tags", "visibility",
|
|
],
|
|
type: "object",
|
|
initialValue: {}, // See create instance handler in controller.
|
|
// TODO extendedValidation, we could check keys and types here if we wanted
|
|
},
|
|
"factorio.verbose_logging": {
|
|
description: "Enable verbose logging on the Factorio server",
|
|
restartRequired: true, // Because of the cli option "--verbose"
|
|
type: "boolean",
|
|
initialValue: false,
|
|
},
|
|
"factorio.console_logging": {
|
|
description: "Enable console logging to a separate file, useful for 3rd party integrations",
|
|
restartRequired: true, // Because of the cli option "--console-log"
|
|
type: "boolean",
|
|
initialValue: false,
|
|
},
|
|
"factorio.strip_paths": {
|
|
description: "Strip down instance paths in the log",
|
|
restartRequired: true,
|
|
type: "boolean",
|
|
initialValue: true,
|
|
},
|
|
"factorio.sync_adminlist": {
|
|
description: "Synchronize adminlist with the controller",
|
|
type: "string",
|
|
enum: ["disabled", "enabled", "bidirectional"],
|
|
initialValue: "bidirectional",
|
|
},
|
|
"factorio.sync_whitelist": {
|
|
description: "Synchronize whitelist with the controller (whitelist must be enabled)",
|
|
type: "string",
|
|
enum: ["disabled", "enabled", "bidirectional"],
|
|
initialValue: "bidirectional",
|
|
},
|
|
"factorio.sync_banlist": {
|
|
description: "Synchronize banlist with the controller",
|
|
type: "string",
|
|
enum: ["disabled", "enabled", "bidirectional"],
|
|
initialValue: "bidirectional",
|
|
},
|
|
"factorio.max_concurrent_commands": {
|
|
description: "Maximum number of RCON commands trasmitted in parallel",
|
|
restartRequired: true,
|
|
type: "number",
|
|
initialValue: 5,
|
|
validator: validators.greaterThanZero,
|
|
},
|
|
};
|
|
}
|
|
|
|
export interface CtlConfigFields {
|
|
"ctl.controller_url": string | null;
|
|
"ctl.controller_token": string | null;
|
|
"ctl.max_reconnect_delay": number;
|
|
}
|
|
|
|
/**
|
|
* Ctl config class
|
|
* @extends classes.Config
|
|
*/
|
|
export class CtlConfig extends classes.Config<CtlConfigFields> {
|
|
declare static fromJSON: configFromJSON<CtlConfig>;
|
|
declare static fromFile: configFromFile<CtlConfig>;
|
|
static fieldDefinitions: classes.ConfigDefs<CtlConfigFields> = {
|
|
"ctl.controller_url": {
|
|
description: "URL to connect to the controller at",
|
|
type: "string",
|
|
optional: true,
|
|
},
|
|
"ctl.controller_token": {
|
|
access: ["control"],
|
|
description: "Token to authenticate to controller with.",
|
|
type: "string",
|
|
optional: true,
|
|
},
|
|
"ctl.max_reconnect_delay": {
|
|
title: "Max Reconnect Delay",
|
|
description: "Maximum delay to wait before attempting to reconnect WebSocket",
|
|
type: "number",
|
|
initialValue: 60,
|
|
validator: validators.greaterThanEqualZero,
|
|
},
|
|
};
|
|
}
|
|
|
|
|
|
function validateFieldNames(
|
|
pluginName: string,
|
|
fields: Record<string, classes.FieldDefinition>,
|
|
) {
|
|
for (const [name, field] of Object.entries(fields)) {
|
|
if (!name.startsWith(`${pluginName}.`)) {
|
|
throw new Error(
|
|
`Expected name of config field '${name}' for ${pluginName} to start with '${pluginName}.'`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add the config fields defined by the by a specific plugin infos
|
|
*/
|
|
export function addPluginFieldDefinitions(
|
|
pluginInfo: PluginNodeEnvInfo | PluginWebEnvInfo,
|
|
kind: "controllerConfigFields" | "hostConfigFields" | "instanceConfigFields",
|
|
Config: typeof classes.Config<any>
|
|
) {
|
|
// Define plugin.load_plugin
|
|
const fieldDef: classes.FieldDefinition = {
|
|
title: "Load Plugin",
|
|
type: "boolean",
|
|
initialValue: true,
|
|
restartRequired: true,
|
|
};
|
|
|
|
// The validator is dynamic based on the feature requirements of the plugin
|
|
if (kind === "instanceConfigFields" && pluginInfo.features) {
|
|
fieldDef.dependsOn = [];
|
|
|
|
const savePatching = pluginInfo.features.includes("SavePatching");
|
|
if (savePatching) {
|
|
fieldDef.dependsOn.push("factorio.enable_save_patching");
|
|
}
|
|
|
|
const scriptCommands = pluginInfo.features.includes("ScriptCommands");
|
|
if (scriptCommands) {
|
|
fieldDef.dependsOn.push("factorio.enable_script_commands");
|
|
}
|
|
|
|
const luaUdp = pluginInfo.features.includes("LuaUdp");
|
|
if (luaUdp) {
|
|
fieldDef.dependsOn.push("factorio.enable_lua_udp");
|
|
}
|
|
|
|
if (scriptCommands || savePatching || luaUdp) {
|
|
fieldDef.validator = (enabled, config) => {
|
|
if (!enabled) {
|
|
return;
|
|
}
|
|
if (savePatching && !config.get("factorio.enable_save_patching")) {
|
|
throw new Error(`Plugin ${pluginInfo.name} requires save patching`);
|
|
}
|
|
if (scriptCommands && !config.get("factorio.enable_script_commands")) {
|
|
throw new Error(`Plugin ${pluginInfo.name} requires script commands`);
|
|
}
|
|
if (luaUdp && !config.get("factorio.enable_lua_udp")) {
|
|
throw new Error(`Plugin ${pluginInfo.name} requires Lua UDP`);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
Config.fieldDefinitions[`${pluginInfo.name}.load_plugin`] = fieldDef;
|
|
|
|
// Version of the plugin the config was last loaded with, see Config.pluginVersions
|
|
Config.fieldDefinitions[`${pluginInfo.name}.version`] = {
|
|
type: "string",
|
|
initialValue: "",
|
|
readonly: ["controller", "host"],
|
|
hidden: true,
|
|
};
|
|
|
|
// Merge in other fields from the plugin
|
|
const fields = pluginInfo[kind];
|
|
if (fields) {
|
|
validateFieldNames(pluginInfo.name, fields);
|
|
Object.assign(Config.fieldDefinitions, fields);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add all config fields defined by the provided plugin infos
|
|
*
|
|
* @param {Array<Object>} pluginInfos - Array of plugin info objects.
|
|
*/
|
|
export function addPluginConfigFields(pluginInfos: PluginNodeEnvInfo[] | PluginWebEnvInfo[]) {
|
|
for (let pluginInfo of pluginInfos) {
|
|
addPluginFieldDefinitions(pluginInfo, "controllerConfigFields", ControllerConfig);
|
|
if (pluginInfo.hostEntrypoint || pluginInfo.instanceEntrypoint) {
|
|
addPluginFieldDefinitions(pluginInfo, "hostConfigFields", HostConfig);
|
|
}
|
|
if (pluginInfo.instanceEntrypoint) {
|
|
addPluginFieldDefinitions(pluginInfo, "instanceConfigFields", InstanceConfig);
|
|
}
|
|
if (pluginInfo.ctlConfigFields) {
|
|
validateFieldNames(pluginInfo.name, pluginInfo.ctlConfigFields);
|
|
Object.assign(CtlConfig.fieldDefinitions, pluginInfo.ctlConfigFields);
|
|
}
|
|
}
|
|
}
|
|
|