1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 inherit (lib)
9 mkEnableOption
10 mkIf
11 mkOption
12 mkMerge
13 literalExpression
14 ;
15 inherit (lib)
16 mapAttrsToList
17 filterAttrs
18 unique
19 recursiveUpdate
20 types
21 ;
22
23 mkValueStringArmagetron =
24 with lib;
25 v:
26 if isInt v then
27 toString v
28 else if isFloat v then
29 toString v
30 else if isString v then
31 v
32 else if true == v then
33 "1"
34 else if false == v then
35 "0"
36 else if null == v then
37 ""
38 else
39 throw "unsupported type: ${builtins.typeOf v}: ${(lib.generators.toPretty { } v)}";
40
41 settingsFormat = pkgs.formats.keyValue {
42 mkKeyValue = lib.generators.mkKeyValueDefault {
43 mkValueString = mkValueStringArmagetron;
44 } " ";
45 listsAsDuplicateKeys = true;
46 };
47
48 cfg = config.services.armagetronad;
49 enabledServers = lib.filterAttrs (n: v: v.enable) cfg.servers;
50 nameToId = serverName: "armagetronad-${serverName}";
51 getStateDirectory = serverName: "armagetronad/${serverName}";
52 getServerRoot = serverName: "/var/lib/${getStateDirectory serverName}";
53in
54{
55 options = {
56 services.armagetronad = {
57 servers = mkOption {
58 description = "Armagetron server definitions.";
59 default = { };
60 type = types.attrsOf (
61 types.submodule {
62 options = {
63 enable = mkEnableOption "armagetronad";
64
65 package = lib.mkPackageOption pkgs "armagetronad-dedicated" {
66 example = ''
67 pkgs.armagetronad."0.2.9-sty+ct+ap".dedicated
68 '';
69 extraDescription = ''
70 Ensure that you use a derivation which contains the path `bin/armagetronad-dedicated`.
71 '';
72 };
73
74 host = mkOption {
75 type = types.str;
76 default = "0.0.0.0";
77 description = "Host to listen on. Used for SERVER_IP.";
78 };
79
80 port = mkOption {
81 type = types.port;
82 default = 4534;
83 description = "Port to listen on. Used for SERVER_PORT.";
84 };
85
86 dns = mkOption {
87 type = types.nullOr types.str;
88 default = null;
89 description = "DNS address to use for this server. Optional.";
90 };
91
92 openFirewall = mkOption {
93 type = types.bool;
94 default = true;
95 description = "Set to true to open the configured UDP port for Armagetron Advanced.";
96 };
97
98 name = mkOption {
99 type = types.str;
100 description = "The name of this server.";
101 };
102
103 settings = mkOption {
104 type = settingsFormat.type;
105 default = { };
106 description = ''
107 Armagetron Advanced server rules configuration. Refer to:
108 <https://wiki.armagetronad.org/index.php?title=Console_Commands>
109 or `armagetronad-dedicated --doc` for a list.
110
111 This attrset is used to populate `settings_custom.cfg`; see:
112 <https://wiki.armagetronad.org/index.php/Configuration_Files>
113 '';
114 example = literalExpression ''
115 {
116 CYCLE_RUBBER = 40;
117 }
118 '';
119 };
120
121 roundSettings = mkOption {
122 type = settingsFormat.type;
123 default = { };
124 description = ''
125 Armagetron Advanced server per-round configuration. Refer to:
126 <https://wiki.armagetronad.org/index.php?title=Console_Commands>
127 or `armagetronad-dedicated --doc` for a list.
128
129 This attrset is used to populate `everytime.cfg`; see:
130 <https://wiki.armagetronad.org/index.php/Configuration_Files>
131 '';
132 example = literalExpression ''
133 {
134 SAY = [
135 "Hosted on NixOS"
136 "https://nixos.org"
137 "iD Tech High Rubber rul3z!! Happy New Year 2008!!1"
138 ];
139 }
140 '';
141 };
142 };
143 }
144 );
145 };
146 };
147 };
148
149 config = mkIf (enabledServers != { }) {
150 systemd.tmpfiles.settings = mkMerge (
151 mapAttrsToList (
152 serverName: serverCfg:
153 let
154 serverId = nameToId serverName;
155 serverRoot = getServerRoot serverName;
156 serverInfo = (
157 {
158 SERVER_IP = serverCfg.host;
159 SERVER_PORT = serverCfg.port;
160 SERVER_NAME = serverCfg.name;
161 }
162 // (lib.optionalAttrs (serverCfg.dns != null) { SERVER_DNS = serverCfg.dns; })
163 );
164 customSettings = serverCfg.settings;
165 everytimeSettings = serverCfg.roundSettings;
166
167 serverInfoCfg = settingsFormat.generate "server_info.${serverName}.cfg" serverInfo;
168 customSettingsCfg = settingsFormat.generate "settings_custom.${serverName}.cfg" customSettings;
169 everytimeSettingsCfg = settingsFormat.generate "everytime.${serverName}.cfg" everytimeSettings;
170 in
171 {
172 "10-armagetronad-${serverId}" = {
173 "${serverRoot}/data" = {
174 d = {
175 group = serverId;
176 user = serverId;
177 mode = "0750";
178 };
179 };
180 "${serverRoot}/settings" = {
181 d = {
182 group = serverId;
183 user = serverId;
184 mode = "0750";
185 };
186 };
187 "${serverRoot}/var" = {
188 d = {
189 group = serverId;
190 user = serverId;
191 mode = "0750";
192 };
193 };
194 "${serverRoot}/resource" = {
195 d = {
196 group = serverId;
197 user = serverId;
198 mode = "0750";
199 };
200 };
201 "${serverRoot}/input" = {
202 "f+" = {
203 group = serverId;
204 user = serverId;
205 mode = "0640";
206 };
207 };
208 "${serverRoot}/settings/server_info.cfg" = {
209 "L+" = {
210 argument = "${serverInfoCfg}";
211 };
212 };
213 "${serverRoot}/settings/settings_custom.cfg" = {
214 "L+" = {
215 argument = "${customSettingsCfg}";
216 };
217 };
218 "${serverRoot}/settings/everytime.cfg" = {
219 "L+" = {
220 argument = "${everytimeSettingsCfg}";
221 };
222 };
223 };
224 }
225 ) enabledServers
226 );
227
228 systemd.services = mkMerge (
229 mapAttrsToList (
230 serverName: serverCfg:
231 let
232 serverId = nameToId serverName;
233 in
234 {
235 "armagetronad-${serverName}" = {
236 description = "Armagetron Advanced Dedicated Server for ${serverName}";
237 wants = [ "basic.target" ];
238 after = [
239 "basic.target"
240 "network.target"
241 "multi-user.target"
242 ];
243 wantedBy = [ "multi-user.target" ];
244 serviceConfig =
245 let
246 serverRoot = getServerRoot serverName;
247 in
248 {
249 Type = "simple";
250 StateDirectory = getStateDirectory serverName;
251 ExecStart = "${lib.getExe serverCfg.package} --daemon --input ${serverRoot}/input --userdatadir ${serverRoot}/data --userconfigdir ${serverRoot}/settings --vardir ${serverRoot}/var --autoresourcedir ${serverRoot}/resource";
252 Restart = "on-failure";
253 CapabilityBoundingSet = "";
254 LockPersonality = true;
255 NoNewPrivileges = true;
256 PrivateDevices = true;
257 PrivateTmp = true;
258 PrivateUsers = true;
259 ProtectClock = true;
260 ProtectControlGroups = true;
261 ProtectHome = true;
262 ProtectHostname = true;
263 ProtectKernelLogs = true;
264 ProtectKernelModules = true;
265 ProtectKernelTunables = true;
266 ProtectProc = "invisible";
267 ProtectSystem = "strict";
268 RestrictNamespaces = true;
269 RestrictSUIDSGID = true;
270 User = serverId;
271 Group = serverId;
272 };
273 };
274 }
275 ) enabledServers
276 );
277
278 networking.firewall.allowedUDPPorts = unique (
279 mapAttrsToList (serverName: serverCfg: serverCfg.port) (
280 filterAttrs (serverName: serverCfg: serverCfg.openFirewall) enabledServers
281 )
282 );
283
284 users.users = mkMerge (
285 mapAttrsToList (serverName: serverCfg: {
286 ${nameToId serverName} = {
287 group = nameToId serverName;
288 description = "Armagetron Advanced dedicated user for server ${serverName}";
289 isSystemUser = true;
290 };
291 }) enabledServers
292 );
293
294 users.groups = mkMerge (
295 mapAttrsToList (serverName: serverCfg: {
296 ${nameToId serverName} = { };
297 }) enabledServers
298 );
299 };
300}