1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.factorio;
7 factorio = pkgs.factorio-headless;
8 name = "Factorio";
9 stateDir = "/var/lib/factorio";
10 mkSavePath = name: "${stateDir}/saves/${name}.zip";
11 configFile = pkgs.writeText "factorio.conf" ''
12 use-system-read-write-data-directories=true
13 [path]
14 read-data=${factorio}/share/factorio/data
15 write-data=${stateDir}
16 '';
17 serverSettings = {
18 name = cfg.game-name;
19 description = cfg.description;
20 visibility = {
21 public = cfg.public;
22 lan = cfg.lan;
23 };
24 username = cfg.username;
25 password = cfg.password;
26 token = cfg.token;
27 game_password = cfg.game-password;
28 require_user_verification = true;
29 max_upload_in_kilobytes_per_second = 0;
30 minimum_latency_in_ticks = 0;
31 ignore_player_limit_for_returning_players = false;
32 allow_commands = "admins-only";
33 autosave_interval = cfg.autosave-interval;
34 autosave_slots = 5;
35 afk_autokick_interval = 0;
36 auto_pause = true;
37 only_admins_can_pause_the_game = true;
38 autosave_only_on_server = true;
39 admins = [];
40 };
41 serverSettingsFile = pkgs.writeText "server-settings.json" (builtins.toJSON (filterAttrsRecursive (n: v: v != null) serverSettings));
42 modDir = pkgs.factorio-utils.mkModDirDrv cfg.mods;
43in
44{
45 options = {
46 services.factorio = {
47 enable = mkEnableOption name;
48 port = mkOption {
49 type = types.int;
50 default = 34197;
51 description = ''
52 The port to which the service should bind.
53
54 This option will also open up the UDP port in the firewall configuration.
55 '';
56 };
57 saveName = mkOption {
58 type = types.string;
59 default = "default";
60 description = ''
61 The name of the savegame that will be used by the server.
62
63 When not present in ${stateDir}/saves, a new map with default
64 settings will be generated before starting the service.
65 '';
66 };
67 # TODO Add more individual settings as nixos-options?
68 # TODO XXX The server tries to copy a newly created config file over the old one
69 # on shutdown, but fails, because it's in the nix store. When is this needed?
70 # Can an admin set options in-game and expect to have them persisted?
71 configFile = mkOption {
72 type = types.path;
73 default = configFile;
74 defaultText = "configFile";
75 description = ''
76 The server's configuration file.
77
78 The default file generated by this module contains lines essential to
79 the server's operation. Use its contents as a basis for any
80 customizations.
81 '';
82 };
83 mods = mkOption {
84 type = types.listOf types.package;
85 default = [];
86 description = ''
87 Mods the server should install and activate.
88
89 The derivations in this list must "build" the mod by simply copying
90 the .zip, named correctly, into the output directory. Eventually,
91 there will be a way to pull in the most up-to-date list of
92 derivations via nixos-channel. Until then, this is for experts only.
93 '';
94 };
95 game-name = mkOption {
96 type = types.nullOr types.string;
97 default = "Factorio Game";
98 description = ''
99 Name of the game as it will appear in the game listing.
100 '';
101 };
102 description = mkOption {
103 type = types.nullOr types.string;
104 default = "";
105 description = ''
106 Description of the game that will appear in the listing.
107 '';
108 };
109 public = mkOption {
110 type = types.bool;
111 default = false;
112 description = ''
113 Game will be published on the official Factorio matching server.
114 '';
115 };
116 lan = mkOption {
117 type = types.bool;
118 default = false;
119 description = ''
120 Game will be broadcast on LAN.
121 '';
122 };
123 username = mkOption {
124 type = types.nullOr types.string;
125 default = null;
126 description = ''
127 Your factorio.com login credentials. Required for games with visibility public.
128 '';
129 };
130 password = mkOption {
131 type = types.nullOr types.string;
132 default = null;
133 description = ''
134 Your factorio.com login credentials. Required for games with visibility public.
135 '';
136 };
137 token = mkOption {
138 type = types.nullOr types.string;
139 default = null;
140 description = ''
141 Authentication token. May be used instead of 'password' above.
142 '';
143 };
144 game-password = mkOption {
145 type = types.nullOr types.string;
146 default = null;
147 description = ''
148 Game password.
149 '';
150 };
151 autosave-interval = mkOption {
152 type = types.nullOr types.int;
153 default = null;
154 example = 10;
155 description = ''
156 Autosave interval in minutes.
157 '';
158 };
159 };
160 };
161
162 config = mkIf cfg.enable {
163 users = {
164 users.factorio = {
165 uid = config.ids.uids.factorio;
166 description = "Factorio server user";
167 group = "factorio";
168 home = stateDir;
169 createHome = true;
170 };
171
172 groups.factorio = {
173 gid = config.ids.gids.factorio;
174 };
175 };
176
177 systemd.services.factorio = {
178 description = "Factorio headless server";
179 wantedBy = [ "multi-user.target" ];
180 after = [ "network.target" ];
181
182 preStart = toString [
183 "test -e ${stateDir}/saves/${cfg.saveName}.zip"
184 "||"
185 "${factorio}/bin/factorio"
186 "--config=${cfg.configFile}"
187 "--create=${mkSavePath cfg.saveName}"
188 (optionalString (cfg.mods != []) "--mod-directory=${modDir}")
189 ];
190
191 serviceConfig = {
192 User = "factorio";
193 Group = "factorio";
194 Restart = "always";
195 KillSignal = "SIGINT";
196 WorkingDirectory = stateDir;
197 PrivateTmp = true;
198 UMask = "0007";
199 ExecStart = toString [
200 "${factorio}/bin/factorio"
201 "--config=${cfg.configFile}"
202 "--port=${toString cfg.port}"
203 "--start-server=${mkSavePath cfg.saveName}"
204 "--server-settings=${serverSettingsFile}"
205 (optionalString (cfg.mods != []) "--mod-directory=${modDir}")
206 ];
207 };
208 };
209
210 networking.firewall.allowedUDPPorts = [ cfg.port ];
211 };
212}