diff options
-rw-r--r-- | modules/module-list.nix | 1 | ||||
-rw-r--r-- | modules/services/starbound.nix | 318 | ||||
-rw-r--r-- | tests/default.nix | 3 | ||||
-rw-r--r-- | tests/games/starbound.nix | 117 |
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"); + ''; +} |