about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--modules/module-list.nix1
-rw-r--r--modules/services/starbound.nix318
-rw-r--r--tests/default.nix3
-rw-r--r--tests/games/starbound.nix117
4 files changed, 439 insertions, 0 deletions
diff --git a/modules/module-list.nix b/modules/module-list.nix
index c4a9e84a..9935021f 100644
--- a/modules/module-list.nix
+++ b/modules/module-list.nix
@@ -7,6 +7,7 @@
   ./profiles/tests.nix
   ./services/multipath-vpn.nix
   ./services/postfix
+  ./services/starbound.nix
   ./system/iso.nix
   ./user/aszlig/profiles/base.nix
   ./user/aszlig/profiles/workstation
diff --git a/modules/services/starbound.nix b/modules/services/starbound.nix
new file mode 100644
index 00000000..2f76eb84
--- /dev/null
+++ b/modules/services/starbound.nix
@@ -0,0 +1,318 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.vuizvui.services.starbound;
+
+  mkListenerOptions = what: defaultPort: {
+    bind = mkOption {
+      type = types.str;
+      default = "::";
+      description = ''
+        Host/IP address to listen for incoming connections to the ${what}.
+      '';
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = defaultPort;
+      description = ''
+        Port to listen for incoming connections to the ${what}.
+      '';
+    };
+  };
+
+  serverConfig = {
+    allowAdminCommands = cfg.adminCommands.allow;
+    allowAdminCommandsFromAnyone = cfg.adminCommands.allowFromAnyone;
+
+    allowAnonymousConnections = cfg.anonymousConnections.allow;
+    anonymousConnectionsAreAdmin = cfg.anonymousConnections.adminPrivileges;
+
+    serverUsers = cfg.users;
+
+    inherit (cfg) checkAssetsDigest clearPlayerFiles clearUniverseFiles;
+    inherit (cfg) maxPlayers safeScripts serverName;
+    inherit (cfg) upnpPortForwarding;
+
+    gameServerBind = cfg.bind;
+    gameServerPort = cfg.port;
+
+    bannedIPs = cfg.bannedIPs;
+    bannedUuids = cfg.bannedUUIDs;
+
+    runRconServer = cfg.rconServer.enable;
+    rconServerBind = cfg.rconServer.bind;
+    rconServerPort = cfg.rconServer.port;
+    rconServerPassword = cfg.rconServer.password;
+    rconServerTimeout = cfg.rconServer.timeout;
+
+    runQueryServer = cfg.queryServer.enable;
+    queryServerBind = cfg.queryServer.bind;
+    queryServerPort = cfg.queryServer.port;
+  } // cfg.extraConfig;
+
+  bootConfig = pkgs.runCommand "sbboot.config" {
+    overrides = pkgs.writeText "sbboot.overrides" (builtins.toJSON {
+      logFileBackups = 0;
+      modSource = "";
+      storageDirectory = cfg.dataDir;
+      defaultConfiguration = serverConfig;
+    });
+  } ''
+    "${pkgs.jq}/bin/jq" -s '.[0] * .[1]' \
+      "${cfg.package}/etc/sbboot.config" "$overrides" \
+      > "$out"
+  '';
+
+  # Traverse a given path with ../ until we get to the root directory (/).
+  gotoRoot = p: concatStringsSep "/" (map (const "..") (splitString "/" p));
+
+in {
+  options.vuizvui.services.starbound = {
+    enable = mkEnableOption "Starbound game server";
+
+    adminCommands = {
+      allow = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to allow admin commands in general.
+        '';
+        # XXX: Make this dependant on whether an account is defined with enabled
+        # admin.
+      };
+
+      allowFromAnyone = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Allow anyone, even anonymous users to use admin commands.
+        '';
+        # XXX: Check whether this is true!
+      };
+    };
+
+    anonymousConnections = {
+      allow = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to allow anonymous connections to the server.
+
+          Set this to <value>false</value> and use <option>serverUsers</option>
+          to only allow specific accounts to connect.
+        '';
+      };
+
+      adminPrivileges = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether all anonymous connections have administrative privileges.
+        '';
+      };
+    };
+
+    users = mkOption {
+      type = types.attrsOf (types.submodule {
+        options.admin = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether this user has admin privileges.
+          '';
+        };
+        options.password = mkOption {
+          type = types.str;
+          example = "supersecure";
+          description = ''
+            The password for the user.
+          '';
+        };
+      });
+    };
+
+    checkAssetsDigest = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Check whether the assets on the client match the ones from the server
+        and deny connection if they don't match.
+      '';
+    };
+
+    clearPlayerFiles = mkOption {
+      # XXX: Figure out the exact semantics of this.
+      type = types.bool;
+      default = false;
+      description = ''
+        Forces players to use new characters or to have no gear or tech.
+      '';
+    };
+
+    clearUniverseFiles = mkOption {
+      # XXX: Figure out the exact semantics of this.
+      type = types.bool;
+      default = false;
+      description = ''
+        Forces player characters to use fresh universe data and navigation maps.
+      '';
+    };
+
+    bannedIPs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        IP addresses disallowed for connection to the server.
+      '';
+    };
+
+    bannedUUIDs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        User IDs disallowed for connection to the server.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/starbound";
+      description = ''
+        The directory where Starbound stores its universe/player files.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.vuizvui.games.steam.starbound;
+      description = ''
+        The starbound package to use for running this game server.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.attrs;
+      default = {};
+      description = ''
+        Extra configuration options to add to the server config.
+      '';
+    };
+
+    rconServer = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run an RCON server which allows to run administrative
+          commands on this game server instance.
+
+          See the <link xlink:href="${
+            "https://developer.valvesoftware.com/wiki/Source_RCON_Protocol"
+          }">RCON protocol documentation</link> for more information about this.
+        '';
+      };
+
+      password = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          The password needed to authorize with the RCON server.
+        '';
+      };
+
+      timeout = mkOption {
+        type = types.int;
+        default = 1000;
+        # XXX: Find out what this timeout is for and whether it's in seconds.
+        description = ''
+          After how many seconds the RCON server drops the connection.
+        '';
+      };
+    } // mkListenerOptions "RCON server" 21026;
+
+    queryServer = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run a query server that shows information such as currently
+          connected players.
+        '';
+      };
+    } // mkListenerOptions "query server" 21025;
+
+    safeScripts = mkOption {
+      type = types.bool;
+      default = true;
+      # XXX: The description is just a guess and we need to find out what this
+      # really does.
+      description = ''
+        This is to make sure scripts can't call unsafe functions.
+      '';
+    };
+
+    serverName = mkOption {
+      type = types.str;
+      default = "A Starbound Server";
+      example = "My shiny Starbound Server";
+      description = ''
+        A short description or name of the Starbound server to run.
+      '';
+    };
+
+    upnpPortForwarding = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to use UPnP to forward ports from NAT gateways.
+      '';
+    };
+
+    maxPlayers = mkOption {
+      type = types.int;
+      default = 8;
+      description = ''
+        Maximum amount of players to allow concurrently.
+      '';
+    };
+  } // mkListenerOptions "game server" 21025;
+
+  config = mkIf cfg.enable {
+    users.groups.starbound = {
+      gid = config.ids.gids.starbound;
+    };
+
+    users.users.starbound = {
+      uid = config.ids.uids.starbound;
+      description = "Starbound Game Server User";
+      group = "starbound";
+      home = cfg.dataDir;
+      createHome = true;
+    };
+
+    systemd.services.starbound = {
+      description = "Starbound Server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "fs.target" ];
+
+      serviceConfig = {
+        User = "starbound";
+        Group = "starbound";
+        PrivateTmp = true;
+
+        KillSignal = "SIGINT";
+
+        ExecStart = toString [
+          "${cfg.package}/bin/starbound-server"
+          "-bootconfig \"${bootConfig}\""
+          # Workaround to disable logging to file
+          "-logfile \"${gotoRoot cfg.dataDir}/dev/null\""
+          "-verbose"
+        ];
+      };
+    };
+  };
+}
diff --git a/tests/default.nix b/tests/default.nix
index eb4f3b81..54b130a3 100644
--- a/tests/default.nix
+++ b/tests/default.nix
@@ -9,6 +9,9 @@ in {
   aszlig = {
     i3 = callTest ./aszlig/i3.nix;
   };
+  games = {
+    starbound = callTest ./games/starbound.nix;
+  };
   richi235 = {
     # Currently broken
     #multipath-vpn = callTest ./richi235/multipath-vpn.nix;
diff --git a/tests/games/starbound.nix b/tests/games/starbound.nix
new file mode 100644
index 00000000..a2c8ba64
--- /dev/null
+++ b/tests/games/starbound.nix
@@ -0,0 +1,117 @@
+{ pkgs, ... }:
+
+let
+  xdo = { name, description, xdoScript }: let
+    xdoFile = pkgs.writeText "${name}.xdo" ''
+      search --onlyvisible --class starbound
+      windowfocus --sync
+      windowactivate --sync
+      ${xdoScript}
+    '';
+    escapeScreenshot = pkgs.lib.replaceStrings ["-"] ["_"];
+  in ''
+    $client->nest("${description}", sub {
+      $client->screenshot("before_${escapeScreenshot name}");
+      $client->succeed("${pkgs.xdotool}/bin/xdotool '${xdoFile}'");
+    });
+  '';
+
+  clickAt = name: x: y: xdo {
+    name = "click-${name}";
+    description = "clicking on ${name} (coords ${toString x} ${toString y})";
+    xdoScript = ''
+      mousemove --window %1 --sync ${toString x} ${toString y}
+      click --repeat 10 1
+    '';
+  };
+
+  typeText = name: text: xdo {
+    name = "type-${name}";
+    description = "typing `${text}' into Starbound";
+    xdoScript = ''
+      type --delay 200 '${text}'
+    '';
+  };
+
+in {
+  name = "starbound";
+
+  enableOCR = true;
+
+  nodes = {
+    server = {
+      vuizvui.services.starbound = {
+        enable = true;
+        # Use a different dataDir than the default to make
+        # sure everything is still working.
+        dataDir = "/var/lib/starbound-test";
+        users.alice.password = "secret";
+      };
+      virtualisation.memorySize = 1024;
+      networking.interfaces.eth1.ipAddress = "192.168.0.1";
+      networking.interfaces.eth1.prefixLength = 24;
+      networking.firewall.enable = false;
+    };
+
+    client = { pkgs, ... }: {
+      imports = [
+        "${import ../../nixpkgs-path.nix}/nixos/tests/common/x11.nix"
+      ];
+      virtualisation.memorySize = 2047;
+      environment.systemPackages = [ pkgs.vuizvui.games.steam.starbound ];
+      networking.interfaces.eth1.ipAddress = "192.168.0.2";
+      networking.interfaces.eth1.prefixLength = 24;
+      networking.firewall.enable = false;
+    };
+  };
+
+  testScript = ''
+    $server->waitForUnit("starbound.service");
+
+    $client->nest("waiting for client to start up", sub {
+      $client->waitForX;
+      $client->succeed("starbound >&2 &");
+      $client->waitForText(qr/options/i);
+    });
+
+    ${clickAt "multiplayer" 100 460}
+    $client->waitForText(qr/select/i);
+    ${clickAt "new-character" 460 170}
+    $client->waitForText(qr/species/i);
+    ${clickAt "create-character" 600 525}
+    $client->waitForText(qr/select/i);
+    ${clickAt "use-character" 460 170}
+    $client->waitForText(qr/ser[vu]er/i);
+
+    ${clickAt "server-address" 460 272}
+    ${typeText "server-address" "192.168.0.1"}
+
+    ${clickAt "server-account" 490 304}
+    ${typeText "server-account" "alice"}
+
+    ${clickAt "server-password" 490 336}
+    ${typeText "server-password" "secret"}
+
+    ${clickAt "join-server" 495 370}
+
+    $client->waitForText(qr/q[uv]est/i);
+    ${xdo {
+      name = "close-quest-dialog";
+      description = "closing the quest dialog window";
+      xdoScript = ''
+        key Escape
+      '';
+    }}
+    ${xdo {
+      name = "move-right";
+      description = "moving to the right of the ship";
+      xdoScript = ''
+        keydown d
+        sleep 10
+        keyup d
+      '';
+    }}
+
+    $client->screenshot("client");
+  '';
+}