summaryrefslogtreecommitdiffstats
path: root/nixos/modules/services/games/quake3-server.nix
blob: a57535c88a3ae24d47d6529802c656a98d15f31b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
{
  config,
  pkgs,
  lib,
  ...
}:

let
  inherit (lib)
    literalMD
    mkEnableOption
    mkIf
    mkOption
    types
    ;
  cfg = config.services.quake3-server;

  toQuake3Value = value: if lib.isBool value then (if value then "1" else "0") else toString value;

  toQuake3Config =
    settings:
    lib.concatStrings (
      lib.mapAttrsToList (name: value: ''seta ${name} "${toQuake3Value value}"'' + "\n") settings
    );

  configFile = pkgs.writeText "q3ds-extra.cfg" ''
    ${toQuake3Config cfg.settings}
    ${cfg.extraConfig}
  '';

  defaultBaseq3 = pkgs.requireFile rec {
    name = "baseq3";
    hashMode = "recursive";
    sha256 = "5dd8ee09eabd45e80450f31d7a8b69b846f59738726929298d8a813ce5725ed3";
    message = ''
      Unfortunately, we cannot download ${name} automatically.
      Please purchase a legitimate copy of Quake 3 and change into the installation directory.

      You can either add all relevant files to the nix-store like this:
      mkdir /tmp/baseq3
      cp baseq3/pak*.pk3 /tmp/baseq3
      nix-store --add-fixed sha256 --recursive /tmp/baseq3

      Alternatively you can set services.quake3-server.baseq3 to a path and
      copy the baseq3 directory into the .q3a subdirectory of that path.
    '';
  };

  home = pkgs.runCommand "quake3-home" { } ''
    mkdir -p $out/.q3a/baseq3

    for file in ${cfg.baseq3}/*; do
      ln -s $file $out/.q3a/baseq3/$(basename $file)
    done

    ln -s ${configFile} $out/.q3a/baseq3/nix.cfg
  '';
in
{

  imports = [
    (lib.mkRenamedOptionModule
      [ "services" "quake3-server" "port" ]
      [ "services" "quake3-server" "settings" "net_port" ]
    )
  ];

  options = {
    services.quake3-server = {
      enable = mkEnableOption "Quake 3 dedicated server";
      package = lib.mkPackageOption pkgs "ioquake3" { };

      openFirewall = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Open the firewall.
        '';
      };

      settings = mkOption {
        type = types.submodule {
          freeformType = types.attrsOf (
            types.nullOr (
              types.oneOf [
                types.str
                types.bool
                types.int
                types.float
                types.port
              ]
            )
          );
          options = {
            net_port = lib.mkOption {
              type = lib.types.port;
              default = 27960;
              description = "UDP port for the dedicated server to bind to.";
            };
          };
        };
        default = { };
        example = {
          sv_hostname = "My Quake 3 server";
          g_gametype = 0;
          sv_pure = true;
        };
        description = ''
          Quake 3 cvars set via `seta` on server start (i.e. persisted,
          archive-flagged cvars – the vast majority of server settings).
          Note that options changed via RCON will not be persisted. To list
          all possible options, use "cvarlist 1" via RCON.
        '';
      };

      extraConfig = mkOption {
        type = types.lines;
        default = "";
        example = ''
          // map rotation and other things that don't map onto plain cvars
          set d1 "map q3dm1 ; set nextmap vstr d2"
          set d2 "map q3dm7 ; set nextmap vstr d1"
          vstr d1

          // rarely needed: cvars with a non-seta flag
          sets sv_privatePassword "hidden"
        '';
        description = ''
          Extra configuration lines appended after `settings`, for anything
          that isn't a plain persisted cvar: map-rotation scripts, `exec`,
          `vstr`, aliases, or cvars that need `set`/`sets`/`sett`/`setu`
          instead of `seta`. Note that options changed via RCON will not be
          persisted. To list all possible options, use "cvarlist 1" via RCON.
        '';
      };

      baseq3 = mkOption {
        type = types.either types.package types.path;
        default = defaultBaseq3;
        defaultText = literalMD "Manually downloaded Quake 3 installation directory.";
        example = "/var/lib/q3ds";
        description = ''
          Path to the baseq3 files (pak*.pk3). If this is on the nix store (type = package) all .pk3 files should be saved
          in the top-level directory. If this is on another filesystem (e.g /var/lib/baseq3) the .pk3 files are searched in
          $baseq3/.q3a/baseq3/
        '';
      };
    };
  };

  config =
    let
      baseq3InStore = builtins.typeOf cfg.baseq3 == "set";
    in
    mkIf cfg.enable {
      networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ cfg.settings.net_port ];

      systemd.services.q3ds = {
        description = "Quake 3 dedicated server";
        wantedBy = [ "multi-user.target" ];
        after = [ "network.target" ];

        environment.HOME = if baseq3InStore then home else cfg.baseq3;

        serviceConfig = with lib; {
          Restart = "always";
          DynamicUser = true;
          WorkingDirectory = if baseq3InStore then home else cfg.baseq3;

          # It is possible to alter configuration files via RCON. To ensure reproducibility we have to prevent this
          ReadOnlyPaths = if baseq3InStore then home else cfg.baseq3;
          ExecStartPre = optionalString (
            !baseq3InStore
          ) "+${pkgs.coreutils}/bin/cp ${configFile} ${cfg.baseq3}/.q3a/baseq3/nix.cfg";

          ExecStart = "${cfg.package}/bin/ioq3ded +exec nix.cfg";
        };
      };
    };
}