base16-schemes: unstable-2024-06-21 -> unstable-2024-11-12
[NixPkgs.git] / nixos / modules / services / games / factorio.nix
blob3e89c01dc841a6615cd28d8c432d8ec5eebbabed
1 { config, lib, pkgs, ... }:
2 let
3   cfg = config.services.factorio;
4   name = "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
9     [path]
10     read-data=${cfg.package}/share/factorio/data
11     write-data=${stateDir}
12   '';
13   serverSettings = {
14     name = cfg.game-name;
15     description = cfg.description;
16     visibility = {
17       public = cfg.public;
18       lan = cfg.lan;
19     };
20     username = cfg.username;
21     password = cfg.password;
22     token = cfg.token;
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;
30     autosave_slots = 5;
31     afk_autokick_interval = 0;
32     auto_pause = true;
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;
45   options = {
46     services.factorio = {
47       enable = lib.mkEnableOption name;
48       port = lib.mkOption {
49         type = lib.types.port;
50         default = 34197;
51         description = ''
52           The port to which the service should bind.
53         '';
54       };
56       bind = lib.mkOption {
57         type = lib.types.str;
58         default = "0.0.0.0";
59         description = ''
60           The address to which the service should bind.
61         '';
62       };
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;
70         default = [];
71         example = [ "Rseding91" "Oxyd" ];
72         description = ''
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
76           is restarted.
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:
81           `stateDirName`).
82         '';
83       };
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;
90         default = [];
91         example = [ "username" ];
92         description = ''
93           List of player names which will be admin.
94         '';
95       };
97       openFirewall = lib.mkOption {
98         type = lib.types.bool;
99         default = false;
100         description = ''
101           Whether to automatically open the specified UDP port in the firewall.
102         '';
103       };
104       saveName = lib.mkOption {
105         type = lib.types.str;
106         default = "default";
107         description = ''
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.
112         '';
113       };
114       loadLatestSave = lib.mkOption {
115         type = lib.types.bool;
116         default = false;
117         description = ''
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
123           a crash or desync.
124         '';
125       };
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";
134         description = ''
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
139           customizations.
140         '';
141       };
142       extraSettingsFile = lib.mkOption {
143         type = lib.types.nullOr lib.types.path;
144         default = null;
145         description = ''
146           File, which is dynamically applied to server-settings.json before
147           startup.
149           This option should be used for credentials.
151           For example a settings file could contain:
152           ```json
153           {
154             "game-password": "hunter1"
155           }
156           ```
157         '';
158       };
159       stateDirName = lib.mkOption {
160         type = lib.types.str;
161         default = "factorio";
162         description = ''
163           Name of the directory under /var/lib holding the server's data.
165           The configuration and map will be stored here.
166         '';
167       };
168       mods = lib.mkOption {
169         type = lib.types.listOf lib.types.package;
170         default = [];
171         description = ''
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.
178         '';
179       };
180       mods-dat = lib.mkOption {
181         type = lib.types.nullOr lib.types.path;
182         default = null;
183         description = ''
184           Mods settings can be changed by specifying a dat file, in the [mod
185           settings file
186           format](https://wiki.factorio.com/Mod_settings_file_format).
187         '';
188       };
189       game-name = lib.mkOption {
190         type = lib.types.nullOr lib.types.str;
191         default = "Factorio Game";
192         description = ''
193           Name of the game as it will appear in the game listing.
194         '';
195       };
196       description = lib.mkOption {
197         type = lib.types.nullOr lib.types.str;
198         default = "";
199         description = ''
200           Description of the game that will appear in the listing.
201         '';
202       };
203       extraSettings = lib.mkOption {
204         type = lib.types.attrs;
205         default = {};
206         example = { max_players = 64; };
207         description = ''
208           Extra game configuration that will go into server-settings.json
209         '';
210       };
211       public = lib.mkOption {
212         type = lib.types.bool;
213         default = false;
214         description = ''
215           Game will be published on the official Factorio matching server.
216         '';
217       };
218       lan = lib.mkOption {
219         type = lib.types.bool;
220         default = false;
221         description = ''
222           Game will be broadcast on LAN.
223         '';
224       };
225       username = lib.mkOption {
226         type = lib.types.nullOr lib.types.str;
227         default = null;
228         description = ''
229           Your factorio.com login credentials. Required for games with visibility public.
231           This option is insecure. Use extraSettingsFile instead.
232         '';
233       };
234       package = lib.mkPackageOption pkgs "factorio-headless" {
235         example = "factorio-headless-experimental";
236       };
237       password = lib.mkOption {
238         type = lib.types.nullOr lib.types.str;
239         default = null;
240         description = ''
241           Your factorio.com login credentials. Required for games with visibility public.
243           This option is insecure. Use extraSettingsFile instead.
244         '';
245       };
246       token = lib.mkOption {
247         type = lib.types.nullOr lib.types.str;
248         default = null;
249         description = ''
250           Authentication token. May be used instead of 'password' above.
251         '';
252       };
253       game-password = lib.mkOption {
254         type = lib.types.nullOr lib.types.str;
255         default = null;
256         description = ''
257           Game password.
259           This option is insecure. Use extraSettingsFile instead.
260         '';
261       };
262       requireUserVerification = lib.mkOption {
263         type = lib.types.bool;
264         default = true;
265         description = ''
266           When set to true, the server will only allow clients that have a valid factorio.com account.
267         '';
268       };
269       autosave-interval = lib.mkOption {
270         type = lib.types.nullOr lib.types.int;
271         default = null;
272         example = 10;
273         description = ''
274           Autosave interval in minutes.
275         '';
276       };
277       nonBlockingSaving = lib.mkOption {
278         type = lib.types.bool;
279         default = false;
280         description = ''
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.
284         '';
285       };
286     };
287   };
289   config = lib.mkIf cfg.enable {
290     systemd.services.factorio = {
291       description   = "Factorio headless server";
292       wantedBy      = [ "multi-user.target" ];
293       after         = [ "network.target" ];
295       preStart =
296         (toString [
297           "test -e ${stateDir}/saves/${cfg.saveName}.zip"
298           "||"
299           "${cfg.package}/bin/factorio"
300           "--config=${cfg.configFile}"
301           "--create=${mkSavePath cfg.saveName}"
302           (lib.optionalString (cfg.mods != []) "--mod-directory=${modDir}")
303         ])
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"));
308       serviceConfig = {
309         Restart = "always";
310         KillSignal = "SIGINT";
311         DynamicUser = true;
312         StateDirectory = cfg.stateDirName;
313         UMask = "0007";
314         ExecStart = toString [
315           "${cfg.package}/bin/factorio"
316           "--config=${cfg.configFile}"
317           "--port=${toString cfg.port}"
318           "--bind=${cfg.bind}"
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
324           }"
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")
330         ];
332         # Sandboxing
333         NoNewPrivileges = true;
334         PrivateTmp = true;
335         PrivateDevices = true;
336         ProtectSystem = "strict";
337         ProtectHome = true;
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;
345       };
346     };
348     networking.firewall.allowedUDPPorts = lib.optional cfg.openFirewall cfg.port;
349   };