1 { config, lib, pkgs, ... }:
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 = {
12 "spectator/kick" = "2";
20 tournamentModeOptions = {
23 "restrictSpectators" = "2";
26 teeworldsConf = pkgs.writeText "teeworlds.cfg" ''
27 sv_port ${toString cfg.port}
28 sv_register ${bool cfg.register}
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}
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;
100 description = "Whether to open firewall ports for Teeworlds.";
103 name = lib.mkOption {
104 type = lib.types.str;
105 default = "unnamed server";
111 register = lib.mkOption {
112 type = lib.types.bool;
116 Whether the server registers as a public server in the global server list. This is disabled by default for privacy reasons.
120 motd = lib.mkOption {
121 type = lib.types.nullOr lib.types.str;
124 The server's message of the day text.
128 password = lib.mkOption {
129 type = lib.types.nullOr lib.types.str;
132 Password to connect to the server.
136 rconPassword = lib.mkOption {
137 type = lib.types.nullOr lib.types.str;
140 Password to access the remote console. If not set, a randomly generated one is displayed in the server log.
144 port = lib.mkOption {
145 type = lib.types.port;
148 Port the server will listen on.
152 extraOptions = lib.mkOption {
153 type = lib.types.listOf lib.types.str;
156 Extra configuration lines for the {file}`teeworlds.cfg`. See [Teeworlds Documentation](https://www.teeworlds.com/?page=docs&wiki=server_settings).
158 example = [ "sv_map dm1" "sv_gametype dm" ];
162 bindAddr = lib.mkOption {
163 type = lib.types.nullOr lib.types.str;
166 The address the server will bind to.
170 enableHighBandwidth = lib.mkOption {
171 type = lib.types.bool;
174 Whether to enable high bandwidth mode on LAN servers. This will double the amount of bandwidth required for running the server.
178 hostName = lib.mkOption {
179 type = lib.types.nullOr lib.types.str;
182 Hostname for the server.
186 inactivePenalty = lib.mkOption {
187 type = lib.types.enum [ "spectator" "spectator/kick" "kick" ];
188 example = "spectator";
189 default = "spectator/kick";
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
201 kickInactiveSpectators = lib.mkOption {
202 type = lib.types.bool;
205 Whether to kick inactive spectators.
209 inactiveTime = lib.mkOption {
210 type = lib.types.ints.unsigned;
213 The amount of minutes a client has to idle before it is considered inactive.
217 maxClients = lib.mkOption {
218 type = lib.types.ints.unsigned;
221 The maximum amount of clients that can be connected to the server at the same time.
225 maxClientsPerIP = lib.mkOption {
226 type = lib.types.ints.unsigned;
229 The maximum amount of clients with the same IP address that can be connected to the server at the same time.
233 skillLevel = lib.mkOption {
234 type = lib.types.enum [ "casual" "normal" "competitive" ];
237 The skill level shown in the server browser.
241 enableSpamProtection = lib.mkOption {
242 type = lib.types.bool;
245 Whether to enable chat spam protection.
251 gameType = lib.mkOption {
252 type = lib.types.str;
256 The game type to use on the server.
258 The default gametypes are `dm`, `tdm`, `ctf`, `lms`, and `lts`.
263 type = lib.types.str;
267 The map to use on the server.
271 swapTeams = lib.mkOption {
272 type = lib.types.bool;
275 Whether to swap teams each round.
279 enableReadyMode = lib.mkOption {
280 type = lib.types.bool;
283 Whether to enable "ready mode"; where players can pause/unpause the game
284 and start the game in warmup, using their ready state.
288 playerSlots = lib.mkOption {
289 type = lib.types.ints.unsigned;
292 The amount of slots to reserve for players (as opposed to spectators).
296 enablePowerups = lib.mkOption {
297 type = lib.types.bool;
300 Whether to allow powerups such as the ninja.
304 scoreLimit = lib.mkOption {
305 type = lib.types.ints.unsigned;
309 The score limit needed to win a round.
313 restrictSpectators = lib.mkOption {
314 type = lib.types.bool;
317 Whether to restrict access to information such as health, ammo and armour in spectator mode.
321 enableTeamDamage = lib.mkOption {
322 type = lib.types.bool;
325 Whether to enable team damage; whether to allow team mates to inflict damage on one another.
329 timeLimit = lib.mkOption {
330 type = lib.types.ints.unsigned;
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.
338 tournamentMode = lib.mkOption {
339 type = lib.types.enum [ "disable" "enable" "restrictSpectators" ];
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.
347 enableVoteKick = lib.mkOption {
348 type = lib.types.bool;
351 Whether to enable voting to kick players.
355 voteKickBanTime = lib.mkOption {
356 type = lib.types.ints.unsigned;
359 The amount of minutes that a player is banned for if they get kicked by a vote.
363 voteKickMinimumPlayers = lib.mkOption {
364 type = lib.types.ints.unsigned;
367 The minimum amount of players required to start a kick vote.
372 environmentFile = lib.mkOption {
373 type = lib.types.nullOr lib.types.path;
375 example = "/var/lib/teeworlds/teeworlds.env";
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.
384 # snippet of teeworlds-related config
385 services.teeworlds.password = "$TEEWORLDS_PASSWORD";
389 # content of the environment file
390 TEEWORLDS_PASSWORD=verysecretpassword
393 Note that this file needs to be available on the host on which
394 `teeworlds` is running.
401 config = lib.mkIf cfg.enable {
402 networking.firewall = lib.mkIf cfg.openPorts {
403 allowedUDPPorts = [ cfg.port ];
406 systemd.services.teeworlds = {
407 description = "Teeworlds Server";
408 wantedBy = [ "multi-user.target" ];
409 after = [ "network.target" ];
413 RuntimeDirectory = "teeworlds";
414 RuntimeDirectoryMode = "0700";
415 EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
417 ${pkgs.envsubst}/bin/envsubst \
418 -i ${teeworldsConf} \
419 -o /run/teeworlds/teeworlds.yaml
421 ExecStart = "${lib.getExe cfg.package} -f /run/teeworlds/teeworlds.yaml";
424 CapabilityBoundingSet = false;
425 PrivateDevices = true;
428 ProtectKernelLogs = true;
429 ProtectKernelModules = true;
430 ProtectKernelTunables = true;
431 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
432 RestrictNamespaces = true;
433 SystemCallArchitectures = "native";