1 { config, lib, pkgs, ... }:
3 cfg = config.services.factorio;
5 stateDir = "/var/lib/${cfg.stateDirName}";
6 mkSavePath = name: "${stateDir}/saves/${name}.zip";
7 configFile = pkgs.writeText "factorio.conf" ''
8 use-system-read-write-data-directories=true
10 read-data=${cfg.package}/share/factorio/data
11 write-data=${stateDir}
15 description = cfg.description;
20 username = cfg.username;
21 password = cfg.password;
23 game_password = cfg.game-password;
24 require_user_verification = cfg.requireUserVerification;
25 max_upload_in_kilobytes_per_second = 0;
26 minimum_latency_in_ticks = 0;
27 ignore_player_limit_for_returning_players = false;
28 allow_commands = "admins-only";
29 autosave_interval = cfg.autosave-interval;
31 afk_autokick_interval = 0;
33 only_admins_can_pause_the_game = true;
34 autosave_only_on_server = true;
35 non_blocking_saving = cfg.nonBlockingSaving;
36 } // cfg.extraSettings;
37 serverSettingsString = builtins.toJSON (lib.filterAttrsRecursive (n: v: v != null) serverSettings);
38 serverSettingsFile = pkgs.writeText "server-settings.json" serverSettingsString;
39 playerListOption = name: list:
40 lib.optionalString (list != [])
41 "--${name}=${pkgs.writeText "${name}.json" (builtins.toJSON list)}";
42 modDir = pkgs.factorio-utils.mkModDirDrv cfg.mods cfg.mods-dat;
47 enable = lib.mkEnableOption name;
49 type = lib.types.port;
52 The port to which the service should bind.
60 The address to which the service should bind.
64 allowedPlayers = lib.mkOption {
65 # I would personally prefer for `allowedPlayers = []` to mean "no-one
66 # can connect" but Factorio seems to ignore empty whitelists (even with
67 # --use-server-whitelist) so we can't implement that behaviour, so we
68 # might as well match theirs.
69 type = lib.types.listOf lib.types.str;
71 example = [ "Rseding91" "Oxyd" ];
73 If non-empty, only these player names are allowed to connect. The game
74 will not be able to save any changes made in-game with the /whitelist
75 console command, though they will still take effect until the server
78 If empty, the whitelist defaults to open, but can be managed with the
79 in-game /whitelist console command (see: /help whitelist), which will
80 cause changes to be saved to the game's state directory (see also:
84 # Opting not to include the banlist in addition the the whitelist because:
85 # - banlists are not as often known in advance,
86 # - losing banlist changes on restart seems much more of a headache.
88 admins = lib.mkOption {
89 type = lib.types.listOf lib.types.str;
91 example = [ "username" ];
93 List of player names which will be admin.
97 openFirewall = lib.mkOption {
98 type = lib.types.bool;
101 Whether to automatically open the specified UDP port in the firewall.
104 saveName = lib.mkOption {
105 type = lib.types.str;
108 The name of the savegame that will be used by the server.
110 When not present in /var/lib/''${config.services.factorio.stateDirName}/saves,
111 a new map with default settings will be generated before starting the service.
114 loadLatestSave = lib.mkOption {
115 type = lib.types.bool;
118 Load the latest savegame on startup. This overrides saveName, in that the latest
119 save will always be used even if a saved game of the given name exists. It still
120 controls the 'canonical' name of the savegame.
122 Set this to true to have the server automatically reload a recent autosave after
126 # TODO Add more individual settings as nixos-options?
127 # TODO XXX The server tries to copy a newly created config file over the old one
128 # on shutdown, but fails, because it's in the nix store. When is this needed?
129 # Can an admin set options in-game and expect to have them persisted?
130 configFile = lib.mkOption {
131 type = lib.types.path;
132 default = configFile;
133 defaultText = lib.literalExpression "configFile";
135 The server's configuration file.
137 The default file generated by this module contains lines essential to
138 the server's operation. Use its contents as a basis for any
142 extraSettingsFile = lib.mkOption {
143 type = lib.types.nullOr lib.types.path;
146 File, which is dynamically applied to server-settings.json before
149 This option should be used for credentials.
151 For example a settings file could contain:
154 "game-password": "hunter1"
159 stateDirName = lib.mkOption {
160 type = lib.types.str;
161 default = "factorio";
163 Name of the directory under /var/lib holding the server's data.
165 The configuration and map will be stored here.
168 mods = lib.mkOption {
169 type = lib.types.listOf lib.types.package;
172 Mods the server should install and activate.
174 The derivations in this list must "build" the mod by simply copying
175 the .zip, named correctly, into the output directory. Eventually,
176 there will be a way to pull in the most up-to-date list of
177 derivations via nixos-channel. Until then, this is for experts only.
180 mods-dat = lib.mkOption {
181 type = lib.types.nullOr lib.types.path;
184 Mods settings can be changed by specifying a dat file, in the [mod
186 format](https://wiki.factorio.com/Mod_settings_file_format).
189 game-name = lib.mkOption {
190 type = lib.types.nullOr lib.types.str;
191 default = "Factorio Game";
193 Name of the game as it will appear in the game listing.
196 description = lib.mkOption {
197 type = lib.types.nullOr lib.types.str;
200 Description of the game that will appear in the listing.
203 extraSettings = lib.mkOption {
204 type = lib.types.attrs;
206 example = { max_players = 64; };
208 Extra game configuration that will go into server-settings.json
211 public = lib.mkOption {
212 type = lib.types.bool;
215 Game will be published on the official Factorio matching server.
219 type = lib.types.bool;
222 Game will be broadcast on LAN.
225 username = lib.mkOption {
226 type = lib.types.nullOr lib.types.str;
229 Your factorio.com login credentials. Required for games with visibility public.
231 This option is insecure. Use extraSettingsFile instead.
234 package = lib.mkPackageOption pkgs "factorio-headless" {
235 example = "factorio-headless-experimental";
237 password = lib.mkOption {
238 type = lib.types.nullOr lib.types.str;
241 Your factorio.com login credentials. Required for games with visibility public.
243 This option is insecure. Use extraSettingsFile instead.
246 token = lib.mkOption {
247 type = lib.types.nullOr lib.types.str;
250 Authentication token. May be used instead of 'password' above.
253 game-password = lib.mkOption {
254 type = lib.types.nullOr lib.types.str;
259 This option is insecure. Use extraSettingsFile instead.
262 requireUserVerification = lib.mkOption {
263 type = lib.types.bool;
266 When set to true, the server will only allow clients that have a valid factorio.com account.
269 autosave-interval = lib.mkOption {
270 type = lib.types.nullOr lib.types.int;
274 Autosave interval in minutes.
277 nonBlockingSaving = lib.mkOption {
278 type = lib.types.bool;
281 Highly experimental feature, enable only at your own risk of losing your saves.
282 On UNIX systems, server will fork itself to create an autosave.
283 Autosaving on connected Windows clients will be disabled regardless of autosave_only_on_server option.
289 config = lib.mkIf cfg.enable {
290 systemd.services.factorio = {
291 description = "Factorio headless server";
292 wantedBy = [ "multi-user.target" ];
293 after = [ "network.target" ];
297 "test -e ${stateDir}/saves/${cfg.saveName}.zip"
299 "${cfg.package}/bin/factorio"
300 "--config=${cfg.configFile}"
301 "--create=${mkSavePath cfg.saveName}"
302 (lib.optionalString (cfg.mods != []) "--mod-directory=${modDir}")
304 + (lib.optionalString (cfg.extraSettingsFile != null) ("\necho ${lib.strings.escapeShellArg serverSettingsString}"
305 + " \"$(cat ${cfg.extraSettingsFile})\" | ${lib.getExe pkgs.jq} -s add"
306 + " > ${stateDir}/server-settings.json"));
310 KillSignal = "SIGINT";
312 StateDirectory = cfg.stateDirName;
314 ExecStart = toString [
315 "${cfg.package}/bin/factorio"
316 "--config=${cfg.configFile}"
317 "--port=${toString cfg.port}"
319 (lib.optionalString (!cfg.loadLatestSave) "--start-server=${mkSavePath cfg.saveName}")
320 "--server-settings=${
321 if (cfg.extraSettingsFile != null)
322 then "${stateDir}/server-settings.json"
323 else serverSettingsFile
325 (lib.optionalString cfg.loadLatestSave "--start-server-load-latest")
326 (lib.optionalString (cfg.mods != []) "--mod-directory=${modDir}")
327 (playerListOption "server-adminlist" cfg.admins)
328 (playerListOption "server-whitelist" cfg.allowedPlayers)
329 (lib.optionalString (cfg.allowedPlayers != []) "--use-server-whitelist")
333 NoNewPrivileges = true;
335 PrivateDevices = true;
336 ProtectSystem = "strict";
338 ProtectControlGroups = true;
339 ProtectKernelModules = true;
340 ProtectKernelTunables = true;
341 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
342 RestrictRealtime = true;
343 RestrictNamespaces = true;
344 MemoryDenyWriteExecute = true;
348 networking.firewall.allowedUDPPorts = lib.optional cfg.openFirewall cfg.port;