1{ config
2, pkgs
3, lib
4, ...
5}:
6
7let
8 cfg = config.services.xonotic;
9
10 serverCfg = pkgs.writeText "xonotic-server.cfg" (
11 toString cfg.prependConfig
12 + "\n"
13 + builtins.concatStringsSep "\n" (
14 lib.mapAttrsToList (key: option:
15 let
16 escape = s: lib.escape [ "\"" ] s;
17 quote = s: "\"${s}\"";
18
19 toValue = x: quote (escape (toString x));
20
21 value = (if lib.isList option then
22 builtins.concatStringsSep
23 " "
24 (builtins.map (x: toValue x) option)
25 else
26 toValue option
27 );
28 in
29 "${key} ${value}"
30 ) cfg.settings
31 )
32 + "\n"
33 + toString cfg.appendConfig
34 );
35in
36
37{
38 options.services.xonotic = {
39 enable = lib.mkEnableOption (lib.mdDoc "Xonotic dedicated server");
40
41 package = lib.mkPackageOption pkgs "xonotic-dedicated" {};
42
43 openFirewall = lib.mkOption {
44 type = lib.types.bool;
45 default = false;
46 description = lib.mdDoc ''
47 Open the firewall for TCP and UDP on the specified port.
48 '';
49 };
50
51 dataDir = lib.mkOption {
52 type = lib.types.path;
53 readOnly = true;
54 default = "/var/lib/xonotic";
55 description = lib.mdDoc ''
56 Data directory.
57 '';
58 };
59
60 settings = lib.mkOption {
61 description = lib.mdDoc ''
62 Generates the `server.cfg` file. Refer to [upstream's example][0] for
63 details.
64
65 [0]: https://gitlab.com/xonotic/xonotic/-/blob/master/server/server.cfg
66 '';
67 default = {};
68 type = lib.types.submodule {
69 freeformType = with lib.types; let
70 scalars = oneOf [ singleLineStr int float ];
71 in
72 attrsOf (oneOf [ scalars (nonEmptyListOf scalars) ]);
73
74 options.sv_public = lib.mkOption {
75 type = lib.types.int;
76 default = 0;
77 example = [ (-1) 1 ];
78 description = lib.mdDoc ''
79 Controls whether the server will be publicly listed.
80 '';
81 };
82
83 options.hostname = lib.mkOption {
84 type = lib.types.singleLineStr;
85 default = "Xonotic $g_xonoticversion Server";
86 description = lib.mdDoc ''
87 The name that will appear in the server list. `$g_xonoticversion`
88 gets replaced with the current version.
89 '';
90 };
91
92 options.sv_motd = lib.mkOption {
93 type = lib.types.singleLineStr;
94 default = "";
95 description = lib.mdDoc ''
96 Text displayed when players join the server.
97 '';
98 };
99
100 options.sv_termsofservice_url = lib.mkOption {
101 type = lib.types.singleLineStr;
102 default = "";
103 description = lib.mdDoc ''
104 URL for the Terms of Service for playing on your server.
105 '';
106 };
107
108 options.maxplayers = lib.mkOption {
109 type = lib.types.int;
110 default = 16;
111 description = lib.mdDoc ''
112 Number of player slots on the server, including spectators.
113 '';
114 };
115
116 options.net_address = lib.mkOption {
117 type = lib.types.singleLineStr;
118 default = "0.0.0.0";
119 description = lib.mdDoc ''
120 The address Xonotic will listen on.
121 '';
122 };
123
124 options.port = lib.mkOption {
125 type = lib.types.port;
126 default = 26000;
127 description = lib.mdDoc ''
128 The port Xonotic will listen on.
129 '';
130 };
131 };
132 };
133
134 # Still useful even though we're using RFC 42 settings because *some* keys
135 # can be repeated.
136 appendConfig = lib.mkOption {
137 type = with lib.types; nullOr lines;
138 default = null;
139 description = lib.mdDoc ''
140 Literal text to insert at the end of `server.cfg`.
141 '';
142 };
143
144 # Certain changes need to happen at the beginning of the file.
145 prependConfig = lib.mkOption {
146 type = with lib.types; nullOr lines;
147 default = null;
148 description = lib.mdDoc ''
149 Literal text to insert at the start of `server.cfg`.
150 '';
151 };
152 };
153
154 config = lib.mkIf cfg.enable {
155 systemd.services.xonotic = {
156 description = "Xonotic server";
157 wantedBy = [ "multi-user.target" ];
158
159 environment = {
160 # Required or else it tries to write the lock file into the nix store
161 HOME = cfg.dataDir;
162 };
163
164 serviceConfig = {
165 DynamicUser = true;
166 User = "xonotic";
167 StateDirectory = "xonotic";
168 ExecStart = "${cfg.package}/bin/xonotic-dedicated";
169
170 # Symlink the configuration from the nix store to where Xonotic actually
171 # looks for it
172 ExecStartPre = [
173 "${pkgs.coreutils}/bin/mkdir -p ${cfg.dataDir}/.xonotic/data"
174 ''
175 ${pkgs.coreutils}/bin/ln -sf ${serverCfg} \
176 ${cfg.dataDir}/.xonotic/data/server.cfg
177 ''
178 ];
179
180 # Cargo-culted from search results about writing Xonotic systemd units
181 ExecReload = "${pkgs.util-linux}/bin/kill -HUP $MAINPID";
182
183 Restart = "on-failure";
184 RestartSec = 10;
185 StartLimitBurst = 5;
186 };
187 };
188
189 networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
190 cfg.settings.port
191 ];
192 networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [
193 cfg.settings.port
194 ];
195 };
196
197 meta.maintainers = with lib.maintainers; [ CobaltCause ];
198}