1 { config, lib, options, pkgs, ... }:
3 cfg = config.services.terraria;
4 opt = options.services.terraria;
5 worldSizeMap = { small = 1; medium = 2; large = 3; };
6 valFlag = name: val: lib.optionalString (val != null) "-${name} \"${lib.escape ["\\" "\""] (toString val)}\"";
7 boolFlag = name: val: lib.optionalString val "-${name}";
9 (valFlag "port" cfg.port)
10 (valFlag "maxPlayers" cfg.maxPlayers)
11 (valFlag "password" cfg.password)
12 (valFlag "motd" cfg.messageOfTheDay)
13 (valFlag "world" cfg.worldPath)
14 (valFlag "autocreate" (builtins.getAttr cfg.autoCreatedWorldSize worldSizeMap))
15 (valFlag "banlist" cfg.banListPath)
16 (boolFlag "secure" cfg.secure)
17 (boolFlag "noupnp" cfg.noUPnP)
20 tmuxCmd = "${lib.getExe pkgs.tmux} -S ${lib.escapeShellArg cfg.dataDir}/terraria.sock";
22 stopScript = pkgs.writeShellScript "terraria-stop" ''
23 if ! [ -d "/proc/$1" ]; then
27 lastline=$(${tmuxCmd} capture-pane -p | grep . | tail -n1)
29 # If the service is not configured to auto-start a world, it will show the world selection prompt
30 # If the last non-empty line on-screen starts with "Choose World", we know the prompt is open
31 if [[ "$lastline" =~ ^'Choose World' ]]; then
32 # In this case, nothing needs to be saved, so we can kill the process
33 ${tmuxCmd} kill-session
35 # Otherwise, we send the `exit` command
36 ${tmuxCmd} send-keys Enter exit Enter
39 # Wait for the process to stop
40 tail --pid="$1" -f /dev/null
46 enable = lib.mkOption {
47 type = lib.types.bool;
50 If enabled, starts a Terraria server. The server can be connected to via `tmux -S ''${config.${opt.dataDir}}/terraria.sock attach`
51 for administration by users who are a part of the `terraria` group (use `C-b d` shortcut to detach again).
56 type = lib.types.port;
59 Specifies the port to listen on.
63 maxPlayers = lib.mkOption {
64 type = lib.types.ints.u8;
67 Sets the max number of players (between 1 and 255).
71 password = lib.mkOption {
72 type = lib.types.nullOr lib.types.str;
75 Sets the server password. Leave `null` for no password.
79 messageOfTheDay = lib.mkOption {
80 type = lib.types.nullOr lib.types.str;
83 Set the server message of the day text.
87 worldPath = lib.mkOption {
88 type = lib.types.nullOr lib.types.path;
91 The path to the world file (`.wld`) which should be loaded.
92 If no world exists at this path, one will be created with the size
93 specified by `autoCreatedWorldSize`.
97 autoCreatedWorldSize = lib.mkOption {
98 type = lib.types.enum [ "small" "medium" "large" ];
101 Specifies the size of the auto-created world if `worldPath` does not
102 point to an existing world.
106 banListPath = lib.mkOption {
107 type = lib.types.nullOr lib.types.path;
110 The path to the ban list.
114 secure = lib.mkOption {
115 type = lib.types.bool;
117 description = "Adds additional cheat protection to the server.";
120 noUPnP = lib.mkOption {
121 type = lib.types.bool;
123 description = "Disables automatic Universal Plug and Play.";
126 openFirewall = lib.mkOption {
127 type = lib.types.bool;
129 description = "Whether to open ports in the firewall";
132 dataDir = lib.mkOption {
133 type = lib.types.str;
134 default = "/var/lib/terraria";
135 example = "/srv/terraria";
136 description = "Path to variable state data directory for terraria.";
141 config = lib.mkIf cfg.enable {
142 users.users.terraria = {
143 description = "Terraria server service user";
147 uid = config.ids.uids.terraria;
150 users.groups.terraria = {
151 gid = config.ids.gids.terraria;
154 systemd.services.terraria = {
155 description = "Terraria Server Service";
156 wantedBy = [ "multi-user.target" ];
157 after = [ "network.target" ];
165 ExecStart = "${tmuxCmd} new -d ${pkgs.terraria-server}/bin/TerrariaServer ${lib.concatStringsSep " " flags}";
166 ExecStop = "${stopScript} $MAINPID";
170 networking.firewall = lib.mkIf cfg.openFirewall {
171 allowedTCPPorts = [ cfg.port ];
172 allowedUDPPorts = [ cfg.port ];