summary refs log tree commit diff
path: root/nixos/modules/services/games/factorio.nix
blob: 96fcd6d2c8b30e5b810374c30670a7f6a190786e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
{ config, lib, pkgs, ... }:

with lib;

let
  cfg = config.services.factorio;
  name = "Factorio";
  stateDir = "/var/lib/${cfg.stateDirName}";
  mkSavePath = name: "${stateDir}/saves/${name}.zip";
  configFile = pkgs.writeText "factorio.conf" ''
    use-system-read-write-data-directories=true
    [path]
    read-data=${cfg.package}/share/factorio/data
    write-data=${stateDir}
  '';
  serverSettings = {
    name = cfg.game-name;
    description = cfg.description;
    visibility = {
      public = cfg.public;
      lan = cfg.lan;
    };
    username = cfg.username;
    password = cfg.password;
    token = cfg.token;
    game_password = cfg.game-password;
    require_user_verification = cfg.requireUserVerification;
    max_upload_in_kilobytes_per_second = 0;
    minimum_latency_in_ticks = 0;
    ignore_player_limit_for_returning_players = false;
    allow_commands = "admins-only";
    autosave_interval = cfg.autosave-interval;
    autosave_slots = 5;
    afk_autokick_interval = 0;
    auto_pause = true;
    only_admins_can_pause_the_game = true;
    autosave_only_on_server = true;
    non_blocking_saving = cfg.nonBlockingSaving;
  } // cfg.extraSettings;
  serverSettingsFile = pkgs.writeText "server-settings.json" (builtins.toJSON (filterAttrsRecursive (n: v: v != null) serverSettings));
  serverAdminsFile = pkgs.writeText "server-adminlist.json" (builtins.toJSON cfg.admins);
  modDir = pkgs.factorio-utils.mkModDirDrv cfg.mods;
in
{
  options = {
    services.factorio = {
      enable = mkEnableOption name;
      port = mkOption {
        type = types.int;
        default = 34197;
        description = ''
          The port to which the service should bind.
        '';
      };

      admins = mkOption {
        type = types.listOf types.str;
        default = [];
        example = [ "username" ];
        description = ''
          List of player names which will be admin.
        '';
      };

      openFirewall = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Whether to automatically open the specified UDP port in the firewall.
        '';
      };
      saveName = mkOption {
        type = types.str;
        default = "default";
        description = ''
          The name of the savegame that will be used by the server.

          When not present in /var/lib/''${config.services.factorio.stateDirName}/saves,
          a new map with default settings will be generated before starting the service.
        '';
      };
      # TODO Add more individual settings as nixos-options?
      # TODO XXX The server tries to copy a newly created config file over the old one
      #   on shutdown, but fails, because it's in the nix store. When is this needed?
      #   Can an admin set options in-game and expect to have them persisted?
      configFile = mkOption {
        type = types.path;
        default = configFile;
        defaultText = literalExpression "configFile";
        description = ''
          The server's configuration file.

          The default file generated by this module contains lines essential to
          the server's operation. Use its contents as a basis for any
          customizations.
        '';
      };
      stateDirName = mkOption {
        type = types.str;
        default = "factorio";
        description = ''
          Name of the directory under /var/lib holding the server's data.

          The configuration and map will be stored here.
        '';
      };
      mods = mkOption {
        type = types.listOf types.package;
        default = [];
        description = ''
          Mods the server should install and activate.

          The derivations in this list must "build" the mod by simply copying
          the .zip, named correctly, into the output directory. Eventually,
          there will be a way to pull in the most up-to-date list of
          derivations via nixos-channel. Until then, this is for experts only.
        '';
      };
      game-name = mkOption {
        type = types.nullOr types.str;
        default = "Factorio Game";
        description = ''
          Name of the game as it will appear in the game listing.
        '';
      };
      description = mkOption {
        type = types.nullOr types.str;
        default = "";
        description = ''
          Description of the game that will appear in the listing.
        '';
      };
      extraSettings = mkOption {
        type = types.attrs;
        default = {};
        example = { admins = [ "username" ];};
        description = ''
          Extra game configuration that will go into server-settings.json
        '';
      };
      public = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Game will be published on the official Factorio matching server.
        '';
      };
      lan = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Game will be broadcast on LAN.
        '';
      };
      username = mkOption {
        type = types.nullOr types.str;
        default = null;
        description = ''
          Your factorio.com login credentials. Required for games with visibility public.
        '';
      };
      package = mkOption {
        type = types.package;
        default = pkgs.factorio-headless;
        defaultText = literalExpression "pkgs.factorio-headless";
        example = literalExpression "pkgs.factorio-headless-experimental";
        description = ''
          Factorio version to use. This defaults to the stable channel.
        '';
      };
      password = mkOption {
        type = types.nullOr types.str;
        default = null;
        description = ''
          Your factorio.com login credentials. Required for games with visibility public.
        '';
      };
      token = mkOption {
        type = types.nullOr types.str;
        default = null;
        description = ''
          Authentication token. May be used instead of 'password' above.
        '';
      };
      game-password = mkOption {
        type = types.nullOr types.str;
        default = null;
        description = ''
          Game password.
        '';
      };
      requireUserVerification = mkOption {
        type = types.bool;
        default = true;
        description = ''
          When set to true, the server will only allow clients that have a valid factorio.com account.
        '';
      };
      autosave-interval = mkOption {
        type = types.nullOr types.int;
        default = null;
        example = 10;
        description = ''
          Autosave interval in minutes.
        '';
      };
      nonBlockingSaving = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Highly experimental feature, enable only at your own risk of losing your saves.
          On UNIX systems, server will fork itself to create an autosave.
          Autosaving on connected Windows clients will be disabled regardless of autosave_only_on_server option.
        '';
      };
    };
  };

  config = mkIf cfg.enable {
    systemd.services.factorio = {
      description   = "Factorio headless server";
      wantedBy      = [ "multi-user.target" ];
      after         = [ "network.target" ];

      preStart = toString [
        "test -e ${stateDir}/saves/${cfg.saveName}.zip"
        "||"
        "${cfg.package}/bin/factorio"
          "--config=${cfg.configFile}"
          "--create=${mkSavePath cfg.saveName}"
          (optionalString (cfg.mods != []) "--mod-directory=${modDir}")
      ];

      serviceConfig = {
        Restart = "always";
        KillSignal = "SIGINT";
        DynamicUser = true;
        StateDirectory = cfg.stateDirName;
        UMask = "0007";
        ExecStart = toString [
          "${cfg.package}/bin/factorio"
          "--config=${cfg.configFile}"
          "--port=${toString cfg.port}"
          "--start-server=${mkSavePath cfg.saveName}"
          "--server-settings=${serverSettingsFile}"
          (optionalString (cfg.mods != []) "--mod-directory=${modDir}")
          (optionalString (cfg.admins != []) "--server-adminlist=${serverAdminsFile}")
        ];

        # Sandboxing
        NoNewPrivileges = true;
        PrivateTmp = true;
        PrivateDevices = true;
        ProtectSystem = "strict";
        ProtectHome = true;
        ProtectControlGroups = true;
        ProtectKernelModules = true;
        ProtectKernelTunables = true;
        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
        RestrictRealtime = true;
        RestrictNamespaces = true;
        MemoryDenyWriteExecute = true;
      };
    };

    networking.firewall.allowedUDPPorts = if cfg.openFirewall then [ cfg.port ] else [];
  };
}