grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / games / teeworlds.nix
blobf12647149573150927a1e74784e1d9eb42a7650b
1 { config, lib, pkgs, ... }:
2 let
3   cfg = config.services.teeworlds;
4   register = cfg.register;
6   bool = b: if b != null && b then "1" else "0";
7   optionalSetting = s: setting: lib.optionalString (s != null) "${setting} ${s}";
8   lookup = attrs: key: default: if attrs ? key then attrs."${key}" else default;
10   inactivePenaltyOptions = {
11     "spectator" = "1";
12     "spectator/kick" = "2";
13     "kick" = "3";
14   };
15   skillLevelOptions = {
16     "casual" = "0";
17     "normal" = "1";
18     "competitive" = "2";
19   };
20   tournamentModeOptions = {
21     "disable" = "0";
22     "enable"  = "1";
23     "restrictSpectators" = "2";
24   };
26   teeworldsConf = pkgs.writeText "teeworlds.cfg" ''
27     sv_port ${toString cfg.port}
28     sv_register ${bool cfg.register}
29     sv_name ${cfg.name}
30     ${optionalSetting cfg.motd "sv_motd"}
31     ${optionalSetting cfg.password "password"}
32     ${optionalSetting cfg.rconPassword "sv_rcon_password"}
34     ${optionalSetting cfg.server.bindAddr "bindaddr"}
35     ${optionalSetting cfg.server.hostName "sv_hostname"}
36     sv_high_bandwidth ${bool cfg.server.enableHighBandwidth}
37     sv_inactivekick ${lookup inactivePenaltyOptions cfg.server.inactivePenalty "spectator/kick"}
38     sv_inactivekick_spec ${bool cfg.server.kickInactiveSpectators}
39     sv_inactivekick_time ${toString cfg.server.inactiveTime}
40     sv_max_clients ${toString cfg.server.maxClients}
41     sv_max_clients_per_ip ${toString cfg.server.maxClientsPerIP}
42     sv_skill_level ${lookup skillLevelOptions cfg.server.skillLevel "normal"}
43     sv_spamprotection ${bool cfg.server.enableSpamProtection}
45     sv_gametype ${cfg.game.gameType}
46     sv_map ${cfg.game.map}
47     sv_match_swap ${bool cfg.game.swapTeams}
48     sv_player_ready_mode ${bool cfg.game.enableReadyMode}
49     sv_player_slots ${toString cfg.game.playerSlots}
50     sv_powerups ${bool cfg.game.enablePowerups}
51     sv_scorelimit ${toString cfg.game.scoreLimit}
52     sv_strict_spectate_mode ${bool cfg.game.restrictSpectators}
53     sv_teamdamage ${bool cfg.game.enableTeamDamage}
54     sv_timelimit ${toString cfg.game.timeLimit}
55     sv_tournament_mode ${lookup tournamentModeOptions cfg.server.tournamentMode "disable"}
56     sv_vote_kick ${bool cfg.game.enableVoteKick}
57     sv_vote_kick_bantime ${toString cfg.game.voteKickBanTime}
58     sv_vote_kick_min ${toString cfg.game.voteKickMinimumPlayers}
60     ${optionalSetting cfg.server.bindAddr "bindaddr"}
61     ${optionalSetting cfg.server.hostName "sv_hostname"}
62     sv_high_bandwidth ${bool cfg.server.enableHighBandwidth}
63     sv_inactivekick ${lookup inactivePenaltyOptions cfg.server.inactivePenalty "spectator/kick"}
64     sv_inactivekick_spec ${bool cfg.server.kickInactiveSpectators}
65     sv_inactivekick_time ${toString cfg.server.inactiveTime}
66     sv_max_clients ${toString cfg.server.maxClients}
67     sv_max_clients_per_ip ${toString cfg.server.maxClientsPerIP}
68     sv_skill_level ${lookup skillLevelOptions cfg.server.skillLevel "normal"}
69     sv_spamprotection ${bool cfg.server.enableSpamProtection}
71     sv_gametype ${cfg.game.gameType}
72     sv_map ${cfg.game.map}
73     sv_match_swap ${bool cfg.game.swapTeams}
74     sv_player_ready_mode ${bool cfg.game.enableReadyMode}
75     sv_player_slots ${toString cfg.game.playerSlots}
76     sv_powerups ${bool cfg.game.enablePowerups}
77     sv_scorelimit ${toString cfg.game.scoreLimit}
78     sv_strict_spectate_mode ${bool cfg.game.restrictSpectators}
79     sv_teamdamage ${bool cfg.game.enableTeamDamage}
80     sv_timelimit ${toString cfg.game.timeLimit}
81     sv_tournament_mode ${lookup tournamentModeOptions cfg.server.tournamentMode "disable"}
82     sv_vote_kick ${bool cfg.game.enableVoteKick}
83     sv_vote_kick_bantime ${toString cfg.game.voteKickBanTime}
84     sv_vote_kick_min ${toString cfg.game.voteKickMinimumPlayers}
86     ${lib.concatStringsSep "\n" cfg.extraOptions}
87   '';
91   options = {
92     services.teeworlds = {
93       enable = lib.mkEnableOption "Teeworlds Server";
95       package = lib.mkPackageOption pkgs "teeworlds-server" { };
97       openPorts = lib.mkOption {
98         type = lib.types.bool;
99         default = false;
100         description = "Whether to open firewall ports for Teeworlds.";
101       };
103       name = lib.mkOption {
104         type = lib.types.str;
105         default = "unnamed server";
106         description = ''
107           Name of the server.
108         '';
109       };
111       register = lib.mkOption {
112         type = lib.types.bool;
113         example = true;
114         default = false;
115         description = ''
116           Whether the server registers as a public server in the global server list. This is disabled by default for privacy reasons.
117         '';
118       };
120       motd = lib.mkOption {
121         type = lib.types.nullOr lib.types.str;
122         default = null;
123         description = ''
124           The server's message of the day text.
125         '';
126       };
128       password = lib.mkOption {
129         type = lib.types.nullOr lib.types.str;
130         default = null;
131         description = ''
132           Password to connect to the server.
133         '';
134       };
136       rconPassword = lib.mkOption {
137         type = lib.types.nullOr lib.types.str;
138         default = null;
139         description = ''
140           Password to access the remote console. If not set, a randomly generated one is displayed in the server log.
141         '';
142       };
144       port = lib.mkOption {
145         type = lib.types.port;
146         default = 8303;
147         description = ''
148           Port the server will listen on.
149         '';
150       };
152       extraOptions = lib.mkOption {
153         type = lib.types.listOf lib.types.str;
154         default = [];
155         description = ''
156           Extra configuration lines for the {file}`teeworlds.cfg`. See [Teeworlds Documentation](https://www.teeworlds.com/?page=docs&wiki=server_settings).
157         '';
158         example = [ "sv_map dm1" "sv_gametype dm" ];
159       };
161       server = {
162         bindAddr = lib.mkOption {
163           type = lib.types.nullOr lib.types.str;
164           default = null;
165           description = ''
166             The address the server will bind to.
167           '';
168         };
170         enableHighBandwidth = lib.mkOption {
171           type = lib.types.bool;
172           default = false;
173           description = ''
174             Whether to enable high bandwidth mode on LAN servers. This will double the amount of bandwidth required for running the server.
175           '';
176         };
178         hostName = lib.mkOption {
179           type = lib.types.nullOr lib.types.str;
180           default = null;
181           description = ''
182             Hostname for the server.
183           '';
184         };
186         inactivePenalty = lib.mkOption {
187           type = lib.types.enum [ "spectator" "spectator/kick" "kick" ];
188           example = "spectator";
189           default = "spectator/kick";
190           description = ''
191             Specify what to do when a client goes inactive (see [](#opt-services.teeworlds.server.inactiveTime)).
193             - `spectator`: send the client into spectator mode
195             - `spectator/kick`: send the client into a free spectator slot, otherwise kick the client
197             - `kick`: kick the client
198           '';
199         };
201         kickInactiveSpectators = lib.mkOption {
202           type = lib.types.bool;
203           default = false;
204           description = ''
205             Whether to kick inactive spectators.
206           '';
207         };
209         inactiveTime = lib.mkOption {
210           type = lib.types.ints.unsigned;
211           default = 3;
212           description = ''
213             The amount of minutes a client has to idle before it is considered inactive.
214           '';
215         };
217         maxClients = lib.mkOption {
218           type = lib.types.ints.unsigned;
219           default = 12;
220           description = ''
221             The maximum amount of clients that can be connected to the server at the same time.
222           '';
223         };
225         maxClientsPerIP = lib.mkOption {
226           type = lib.types.ints.unsigned;
227           default = 12;
228           description = ''
229             The maximum amount of clients with the same IP address that can be connected to the server at the same time.
230           '';
231         };
233         skillLevel = lib.mkOption {
234           type = lib.types.enum [ "casual" "normal" "competitive" ];
235           default = "normal";
236           description = ''
237             The skill level shown in the server browser.
238           '';
239         };
241         enableSpamProtection = lib.mkOption {
242           type = lib.types.bool;
243           default = true;
244           description = ''
245             Whether to enable chat spam protection.
246           '';
247         };
248       };
250       game = {
251         gameType = lib.mkOption {
252           type = lib.types.str;
253           example = "ctf";
254           default = "dm";
255           description = ''
256             The game type to use on the server.
258             The default gametypes are `dm`, `tdm`, `ctf`, `lms`, and `lts`.
259           '';
260         };
262         map = lib.mkOption {
263           type = lib.types.str;
264           example = "ctf5";
265           default = "dm1";
266           description = ''
267             The map to use on the server.
268           '';
269         };
271         swapTeams = lib.mkOption {
272           type = lib.types.bool;
273           default = true;
274           description = ''
275             Whether to swap teams each round.
276           '';
277         };
279         enableReadyMode = lib.mkOption {
280           type = lib.types.bool;
281           default = false;
282           description = ''
283             Whether to enable "ready mode"; where players can pause/unpause the game
284             and start the game in warmup, using their ready state.
285           '';
286         };
288         playerSlots = lib.mkOption {
289           type = lib.types.ints.unsigned;
290           default = 8;
291           description = ''
292             The amount of slots to reserve for players (as opposed to spectators).
293           '';
294         };
296         enablePowerups = lib.mkOption {
297           type = lib.types.bool;
298           default = true;
299           description = ''
300             Whether to allow powerups such as the ninja.
301           '';
302         };
304         scoreLimit = lib.mkOption {
305           type = lib.types.ints.unsigned;
306           example = 400;
307           default = 20;
308           description = ''
309             The score limit needed to win a round.
310           '';
311         };
313         restrictSpectators = lib.mkOption {
314           type = lib.types.bool;
315           default = false;
316           description = ''
317             Whether to restrict access to information such as health, ammo and armour in spectator mode.
318           '';
319         };
321         enableTeamDamage = lib.mkOption {
322           type = lib.types.bool;
323           default = false;
324           description = ''
325             Whether to enable team damage; whether to allow team mates to inflict damage on one another.
326           '';
327         };
329         timeLimit = lib.mkOption {
330           type = lib.types.ints.unsigned;
331           default = 0;
332           description = ''
333             Time limit of the game. In cases of equal points, there will be sudden death.
334             Setting this to 0 disables a time limit.
335           '';
336         };
338         tournamentMode = lib.mkOption {
339           type = lib.types.enum [ "disable" "enable" "restrictSpectators" ];
340           default = "disable";
341           description = ''
342             Whether to enable tournament mode. In tournament mode, players join as spectators.
343             If this is set to `restrictSpectators`, tournament mode is enabled but spectator chat is restricted.
344           '';
345         };
347         enableVoteKick = lib.mkOption {
348           type = lib.types.bool;
349           default = true;
350           description = ''
351             Whether to enable voting to kick players.
352           '';
353         };
355         voteKickBanTime = lib.mkOption {
356           type = lib.types.ints.unsigned;
357           default = 5;
358           description = ''
359             The amount of minutes that a player is banned for if they get kicked by a vote.
360           '';
361         };
363         voteKickMinimumPlayers = lib.mkOption {
364           type = lib.types.ints.unsigned;
365           default = 5;
366           description = ''
367             The minimum amount of players required to start a kick vote.
368           '';
369         };
370       };
372       environmentFile = lib.mkOption {
373         type = lib.types.nullOr lib.types.path;
374         default = null;
375         example = "/var/lib/teeworlds/teeworlds.env";
376         description = ''
377           Environment file as defined in {manpage}`systemd.exec(5)`.
379           Secrets may be passed to the service without adding them to the world-readable
380           Nix store, by specifying placeholder variables as the option value in Nix and
381           setting these variables accordingly in the environment file.
383           ```
384             # snippet of teeworlds-related config
385             services.teeworlds.password = "$TEEWORLDS_PASSWORD";
386           ```
388           ```
389             # content of the environment file
390             TEEWORLDS_PASSWORD=verysecretpassword
391           ```
393           Note that this file needs to be available on the host on which
394           `teeworlds` is running.
395         '';
396       };
398     };
399   };
401   config = lib.mkIf cfg.enable {
402     networking.firewall = lib.mkIf cfg.openPorts {
403       allowedUDPPorts = [ cfg.port ];
404     };
406     systemd.services.teeworlds = {
407       description = "Teeworlds Server";
408       wantedBy = [ "multi-user.target" ];
409       after = [ "network.target" ];
411       serviceConfig = {
412         DynamicUser = true;
413         RuntimeDirectory = "teeworlds";
414         RuntimeDirectoryMode = "0700";
415         EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
416         ExecStartPre = ''
417           ${pkgs.envsubst}/bin/envsubst \
418             -i ${teeworldsConf} \
419             -o /run/teeworlds/teeworlds.yaml
420         '';
421         ExecStart = "${lib.getExe cfg.package} -f /run/teeworlds/teeworlds.yaml";
423         # Hardening
424         CapabilityBoundingSet = false;
425         PrivateDevices = true;
426         PrivateUsers = true;
427         ProtectHome = true;
428         ProtectKernelLogs = true;
429         ProtectKernelModules = true;
430         ProtectKernelTunables = true;
431         RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
432         RestrictNamespaces = true;
433         SystemCallArchitectures = "native";
434       };
435     };
436   };