grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / games / factorio.nix
bloba9a4f386e0609d4b785a449ad8c4847b1a892eab
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   serverAdminsFile = pkgs.writeText "server-adminlist.json" (builtins.toJSON cfg.admins);
40   modDir = pkgs.factorio-utils.mkModDirDrv cfg.mods cfg.mods-dat;
43   options = {
44     services.factorio = {
45       enable = lib.mkEnableOption name;
46       port = lib.mkOption {
47         type = lib.types.port;
48         default = 34197;
49         description = ''
50           The port to which the service should bind.
51         '';
52       };
54       bind = lib.mkOption {
55         type = lib.types.str;
56         default = "0.0.0.0";
57         description = ''
58           The address to which the service should bind.
59         '';
60       };
62       admins = lib.mkOption {
63         type = lib.types.listOf lib.types.str;
64         default = [];
65         example = [ "username" ];
66         description = ''
67           List of player names which will be admin.
68         '';
69       };
71       openFirewall = lib.mkOption {
72         type = lib.types.bool;
73         default = false;
74         description = ''
75           Whether to automatically open the specified UDP port in the firewall.
76         '';
77       };
78       saveName = lib.mkOption {
79         type = lib.types.str;
80         default = "default";
81         description = ''
82           The name of the savegame that will be used by the server.
84           When not present in /var/lib/''${config.services.factorio.stateDirName}/saves,
85           a new map with default settings will be generated before starting the service.
86         '';
87       };
88       loadLatestSave = lib.mkOption {
89         type = lib.types.bool;
90         default = false;
91         description = ''
92           Load the latest savegame on startup. This overrides saveName, in that the latest
93           save will always be used even if a saved game of the given name exists. It still
94           controls the 'canonical' name of the savegame.
96           Set this to true to have the server automatically reload a recent autosave after
97           a crash or desync.
98         '';
99       };
100       # TODO Add more individual settings as nixos-options?
101       # TODO XXX The server tries to copy a newly created config file over the old one
102       #   on shutdown, but fails, because it's in the nix store. When is this needed?
103       #   Can an admin set options in-game and expect to have them persisted?
104       configFile = lib.mkOption {
105         type = lib.types.path;
106         default = configFile;
107         defaultText = lib.literalExpression "configFile";
108         description = ''
109           The server's configuration file.
111           The default file generated by this module contains lines essential to
112           the server's operation. Use its contents as a basis for any
113           customizations.
114         '';
115       };
116       extraSettingsFile = lib.mkOption {
117         type = lib.types.nullOr lib.types.path;
118         default = null;
119         description = ''
120           File, which is dynamically applied to server-settings.json before
121           startup.
123           This option should be used for credentials.
125           For example a settings file could contain:
126           ```json
127           {
128             "game-password": "hunter1"
129           }
130           ```
131         '';
132       };
133       stateDirName = lib.mkOption {
134         type = lib.types.str;
135         default = "factorio";
136         description = ''
137           Name of the directory under /var/lib holding the server's data.
139           The configuration and map will be stored here.
140         '';
141       };
142       mods = lib.mkOption {
143         type = lib.types.listOf lib.types.package;
144         default = [];
145         description = ''
146           Mods the server should install and activate.
148           The derivations in this list must "build" the mod by simply copying
149           the .zip, named correctly, into the output directory. Eventually,
150           there will be a way to pull in the most up-to-date list of
151           derivations via nixos-channel. Until then, this is for experts only.
152         '';
153       };
154       mods-dat = lib.mkOption {
155         type = lib.types.nullOr lib.types.path;
156         default = null;
157         description = ''
158           Mods settings can be changed by specifying a dat file, in the [mod
159           settings file
160           format](https://wiki.factorio.com/Mod_settings_file_format).
161         '';
162       };
163       game-name = lib.mkOption {
164         type = lib.types.nullOr lib.types.str;
165         default = "Factorio Game";
166         description = ''
167           Name of the game as it will appear in the game listing.
168         '';
169       };
170       description = lib.mkOption {
171         type = lib.types.nullOr lib.types.str;
172         default = "";
173         description = ''
174           Description of the game that will appear in the listing.
175         '';
176       };
177       extraSettings = lib.mkOption {
178         type = lib.types.attrs;
179         default = {};
180         example = { admins = [ "username" ];};
181         description = ''
182           Extra game configuration that will go into server-settings.json
183         '';
184       };
185       public = lib.mkOption {
186         type = lib.types.bool;
187         default = false;
188         description = ''
189           Game will be published on the official Factorio matching server.
190         '';
191       };
192       lan = lib.mkOption {
193         type = lib.types.bool;
194         default = false;
195         description = ''
196           Game will be broadcast on LAN.
197         '';
198       };
199       username = lib.mkOption {
200         type = lib.types.nullOr lib.types.str;
201         default = null;
202         description = ''
203           Your factorio.com login credentials. Required for games with visibility public.
205           This option is insecure. Use extraSettingsFile instead.
206         '';
207       };
208       package = lib.mkPackageOption pkgs "factorio-headless" {
209         example = "factorio-headless-experimental";
210       };
211       password = lib.mkOption {
212         type = lib.types.nullOr lib.types.str;
213         default = null;
214         description = ''
215           Your factorio.com login credentials. Required for games with visibility public.
217           This option is insecure. Use extraSettingsFile instead.
218         '';
219       };
220       token = lib.mkOption {
221         type = lib.types.nullOr lib.types.str;
222         default = null;
223         description = ''
224           Authentication token. May be used instead of 'password' above.
225         '';
226       };
227       game-password = lib.mkOption {
228         type = lib.types.nullOr lib.types.str;
229         default = null;
230         description = ''
231           Game password.
233           This option is insecure. Use extraSettingsFile instead.
234         '';
235       };
236       requireUserVerification = lib.mkOption {
237         type = lib.types.bool;
238         default = true;
239         description = ''
240           When set to true, the server will only allow clients that have a valid factorio.com account.
241         '';
242       };
243       autosave-interval = lib.mkOption {
244         type = lib.types.nullOr lib.types.int;
245         default = null;
246         example = 10;
247         description = ''
248           Autosave interval in minutes.
249         '';
250       };
251       nonBlockingSaving = lib.mkOption {
252         type = lib.types.bool;
253         default = false;
254         description = ''
255           Highly experimental feature, enable only at your own risk of losing your saves.
256           On UNIX systems, server will fork itself to create an autosave.
257           Autosaving on connected Windows clients will be disabled regardless of autosave_only_on_server option.
258         '';
259       };
260     };
261   };
263   config = lib.mkIf cfg.enable {
264     systemd.services.factorio = {
265       description   = "Factorio headless server";
266       wantedBy      = [ "multi-user.target" ];
267       after         = [ "network.target" ];
269       preStart =
270         (toString [
271           "test -e ${stateDir}/saves/${cfg.saveName}.zip"
272           "||"
273           "${cfg.package}/bin/factorio"
274           "--config=${cfg.configFile}"
275           "--create=${mkSavePath cfg.saveName}"
276           (lib.optionalString (cfg.mods != []) "--mod-directory=${modDir}")
277         ])
278         + (lib.optionalString (cfg.extraSettingsFile != null) ("\necho ${lib.strings.escapeShellArg serverSettingsString}"
279           + " \"$(cat ${cfg.extraSettingsFile})\" | ${lib.getExe pkgs.jq} -s add"
280           + " > ${stateDir}/server-settings.json"));
282       serviceConfig = {
283         Restart = "always";
284         KillSignal = "SIGINT";
285         DynamicUser = true;
286         StateDirectory = cfg.stateDirName;
287         UMask = "0007";
288         ExecStart = toString [
289           "${cfg.package}/bin/factorio"
290           "--config=${cfg.configFile}"
291           "--port=${toString cfg.port}"
292           "--bind=${cfg.bind}"
293           (lib.optionalString (!cfg.loadLatestSave) "--start-server=${mkSavePath cfg.saveName}")
294           "--server-settings=${
295             if (cfg.extraSettingsFile != null)
296             then "${stateDir}/server-settings.json"
297             else serverSettingsFile
298           }"
299           (lib.optionalString cfg.loadLatestSave "--start-server-load-latest")
300           (lib.optionalString (cfg.mods != []) "--mod-directory=${modDir}")
301           (lib.optionalString (cfg.admins != []) "--server-adminlist=${serverAdminsFile}")
302         ];
304         # Sandboxing
305         NoNewPrivileges = true;
306         PrivateTmp = true;
307         PrivateDevices = true;
308         ProtectSystem = "strict";
309         ProtectHome = true;
310         ProtectControlGroups = true;
311         ProtectKernelModules = true;
312         ProtectKernelTunables = true;
313         RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
314         RestrictRealtime = true;
315         RestrictNamespaces = true;
316         MemoryDenyWriteExecute = true;
317       };
318     };
320     networking.firewall.allowedUDPPorts = lib.optional cfg.openFirewall cfg.port;
321   };