mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
950 lines
31 KiB
TypeScript
950 lines
31 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.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.host_pairing_timeout": number;
|
|
"controller.host_pairing_max_pending": 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.host_pairing_timeout": {
|
|
title: "Host Pairing Timeout",
|
|
description: "Time in seconds a host can wait for approval before it has to request again, 0 for no limit.",
|
|
type: "number",
|
|
initialValue: 3600,
|
|
validator: validators.greaterThanEqualZero,
|
|
},
|
|
"controller.host_pairing_max_pending": {
|
|
title: "Host Pairing Max Pending",
|
|
description: "Maximum number of hosts that can wait for approval at the same time, 0 for no limit.",
|
|
type: "number",
|
|
initialValue: 50,
|
|
validator: validators.all(validators.integer, validators.greaterThanEqualZero),
|
|
},
|
|
"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.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: "34100-34199",
|
|
// 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.exclude_from_start_all": boolean;
|
|
"instance.is_template": boolean;
|
|
"instance.template_id": number | null;
|
|
"instance.inherited_fields": Record<string, true | string[]>;
|
|
[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;
|
|
}
|
|
|
|
/** Fields that are never inherited from a template, ports clash on a shared host and secrets stay per instance */
|
|
static uninheritableFields = [
|
|
"instance.name", "instance.is_template", "instance.template_id",
|
|
"factorio.game_port", "factorio.rcon_port", "factorio.rcon_password", "factorio.lua_udp_port",
|
|
];
|
|
|
|
/** True if the field can take its value from a template */
|
|
static isInheritable(name: string) {
|
|
const def = this.fieldDefinitions[name as keyof InstanceConfigFields];
|
|
return Boolean(def)
|
|
&& !def.hidden
|
|
&& (def.access ?? this.defaultAccess).includes("controller")
|
|
&& !this.uninheritableFields.includes(name);
|
|
}
|
|
|
|
/**
|
|
* True if the field, or the given property of an object field, takes its value from the template
|
|
*
|
|
* @param name - Name of field to check.
|
|
* @param prop - Property of an object field to check.
|
|
*/
|
|
isInherited(name: string, prop?: string) {
|
|
if (this.get("instance.template_id") === null) {
|
|
return false;
|
|
}
|
|
const entry = this.get("instance.inherited_fields")[name];
|
|
if (entry instanceof Array) {
|
|
return prop === undefined || !entry.includes(prop);
|
|
}
|
|
return Boolean(entry);
|
|
}
|
|
|
|
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.exclude_from_start_all": {
|
|
description: "Exclude this instance from the 'Start all' button operation",
|
|
type: "boolean",
|
|
initialValue: false,
|
|
},
|
|
"instance.is_template": {
|
|
description:
|
|
"Mark this instance as a template other instances can inherit config from. " +
|
|
"Templates cannot be assigned to a host or started.",
|
|
access: ["controller", "control"],
|
|
type: "boolean",
|
|
initialValue: false,
|
|
hidden: true,
|
|
},
|
|
"instance.template_id": {
|
|
title: "Template",
|
|
description: "ID of the template to inherit config values from",
|
|
access: ["controller", "control"],
|
|
inputComponent: "instance_template",
|
|
type: "number",
|
|
optional: true,
|
|
// Checked by the controller
|
|
},
|
|
"instance.inherited_fields": {
|
|
description:
|
|
"Fields whose value comes from the template, for object fields the properties set on this instance",
|
|
access: ["controller", "control"],
|
|
readonly: ["controller"],
|
|
type: "object",
|
|
initialValue: {},
|
|
hidden: true,
|
|
},
|
|
"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) {
|
|
const requirements: [string, string][] = [];
|
|
if (pluginInfo.features.includes("SavePatching")) {
|
|
requirements.push(["factorio.enable_save_patching", "save patching"]);
|
|
}
|
|
if (pluginInfo.features.includes("ScriptCommands")) {
|
|
requirements.push(["factorio.enable_script_commands", "script commands"]);
|
|
}
|
|
if (pluginInfo.features.includes("LuaUdp")) {
|
|
requirements.push(["factorio.enable_lua_udp", "Lua UDP"]);
|
|
}
|
|
|
|
if (requirements.length) {
|
|
fieldDef.dependsOn = requirements.map(([field]) => field);
|
|
// Default to not loaded when the instance does not meet the requirements
|
|
fieldDef.initialValue = (config: classes.Config<any>) => requirements.every(
|
|
([field]) => config.get(field)
|
|
);
|
|
fieldDef.validator = (enabled, config) => {
|
|
if (!enabled) {
|
|
return;
|
|
}
|
|
for (const [field, description] of requirements) {
|
|
if (!config.get(field)) {
|
|
throw new Error(`Plugin ${pluginInfo.name} requires ${description}`);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|