diff --git a/configs/nixos/guests/palworld.nix b/configs/nixos/guests/palworld.nix new file mode 100644 index 0000000..212a20d --- /dev/null +++ b/configs/nixos/guests/palworld.nix @@ -0,0 +1,146 @@ +{ + config, + lib, + ... +}: +let + cfg = config.guests.palworld; + steamDir = "/data/steamcmd"; + installDir = "/data/server"; + stateDir = "/data/state"; +in +{ + options.guests.palworld = { + steamPath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/srv/steamcmd"; + description = "Host path bind-mounted as SteamCMD's metadata directory."; + }; + + installPath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/srv/palworld/server"; + description = "Host path bind-mounted as Palworld's server install directory."; + }; + + statePath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/srv/palworld/state"; + description = "Host path bind-mounted as Palworld's durable state directory."; + }; + + serverName = lib.mkOption { + type = lib.types.str; + default = "palworld"; + description = "Name shown for the Palworld server."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 8211; + description = "UDP port for the Palworld server."; + }; + + publicIP = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Public IP address advertised by the Palworld server."; + }; + + passwordSecret = lib.mkOption { + type = lib.types.str; + default = "services/game-servers/password"; + description = "SOPS secret name containing the Palworld server password."; + }; + + adminPasswordSecret = lib.mkOption { + type = lib.types.str; + default = "services/game-servers/password"; + description = "SOPS secret name containing the Palworld admin password."; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.steamPath != null; + message = "guests.palworld.steamPath must point at the host directory that stores SteamCMD metadata."; + } + { + assertion = cfg.installPath != null; + message = "guests.palworld.installPath must point at the host directory that stores Palworld server files."; + } + { + assertion = cfg.statePath != null; + message = "guests.palworld.statePath must point at the host directory that stores Palworld durable state."; + } + ]; + + guests.palworld = { + interior.modules.services.games.palworld = { + enable = true; + inherit (cfg) serverName port publicIP; + inherit steamDir installDir stateDir; + passwordFile = "/run/secrets/${cfg.passwordSecret}"; + adminPasswordFile = "/run/secrets/${cfg.adminPasswordSecret}"; + }; + + mounts = + { + ${steamDir} = { + directory = cfg.steamPath; + owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd; + group = "storage"; + mode = "0770"; + }; + ${installDir} = { + directory = cfg.installPath; + owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd; + group = "storage"; + mode = "0770"; + }; + ${stateDir} = { + directory = cfg.statePath; + owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd; + group = "storage"; + mode = "0770"; + }; + ${"/run/secrets/${cfg.passwordSecret}"} = { + file = config.sops.secrets.${cfg.passwordSecret}.path; + readOnly = true; + }; + } + // lib.optionalAttrs (cfg.adminPasswordSecret != cfg.passwordSecret) { + ${"/run/secrets/${cfg.adminPasswordSecret}"} = { + file = config.sops.secrets.${cfg.adminPasswordSecret}.path; + readOnly = true; + }; + }; + + limits = { + memory = lib.mkDefault "8G"; + cpu = lib.mkDefault "400%"; + tasksMax = lib.mkDefault 2048; + }; + }; + + sops.secrets = + { + ${cfg.passwordSecret} = { + owner = "root"; + group = "storage"; + mode = "0440"; + }; + } + // lib.optionalAttrs (cfg.adminPasswordSecret != cfg.passwordSecret) { + ${cfg.adminPasswordSecret} = { + owner = "root"; + group = "storage"; + mode = "0440"; + }; + }; + }; +} diff --git a/configs/nixos/hosts/pikachu/default.nix b/configs/nixos/hosts/pikachu/default.nix index d5854e3..715182d 100644 --- a/configs/nixos/hosts/pikachu/default.nix +++ b/configs/nixos/hosts/pikachu/default.nix @@ -86,6 +86,24 @@ }; }; + guests.palworld = { + enable = true; + vlan = 20; + address = "10.23.20.198"; + steamPath = "/pikachu/data/steamcmd"; + installPath = "/pikachu/data/servers/palworld/tracion"; + statePath = "/pikachu/media/games/servers/palworld/tracion"; + serverName = "Tracion"; + publicIP = config.guests.palworld.address; + reverseProxy = { + enable = true; + route = { + domain = "palworld.alexion.dev"; + udp.ports = [ config.guests.palworld.port ]; + }; + }; + }; + guests.actualbudget = { enable = true; vlan = 20; diff --git a/configs/nixos/modules/services/games/palworld.nix b/configs/nixos/modules/services/games/palworld.nix new file mode 100644 index 0000000..7c321da --- /dev/null +++ b/configs/nixos/modules/services/games/palworld.nix @@ -0,0 +1,148 @@ +{ + config, + lib, + pkgs, + ... +}: +{ + options.modules.services.games.palworld = { + enable = lib.mkEnableOption "Palworld dedicated server"; + + steamDir = lib.mkOption { + type = lib.types.str; + example = "/data/steamcmd"; + description = "Directory where SteamCMD stores metadata and cache files."; + }; + + installDir = lib.mkOption { + type = lib.types.str; + example = "/data/servers/palworld/tracion"; + description = "Directory where SteamCMD installs the Palworld server files."; + }; + + stateDir = lib.mkOption { + type = lib.types.str; + example = "/data/state/palworld/tracion"; + description = "Directory where Palworld stores durable server state."; + }; + + serverName = lib.mkOption { + type = lib.types.str; + default = "palworld"; + description = "Name shown for the Palworld server."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 8211; + description = "UDP port for the Palworld server."; + }; + + publicIP = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Public IP address advertised by the Palworld server."; + }; + + maxPlayers = lib.mkOption { + type = lib.types.ints.between 1 32; + default = 32; + description = "Maximum player count for the Palworld server."; + }; + + publicLobby = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Advertise the server in Palworld's public lobby."; + }; + + rcon = { + enable = lib.mkEnableOption "Palworld RCON" // { + default = true; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 25575; + description = "TCP port for Palworld RCON."; + }; + }; + + passwordFile = lib.mkOption { + type = lib.types.path; + description = "Runtime file containing the Palworld server password."; + }; + + adminPasswordFile = lib.mkOption { + type = lib.types.path; + description = "Runtime file containing the Palworld admin password."; + }; + }; + + config = lib.mkIf config.modules.services.games.palworld.enable ( + let + service = config.modules.services.games.palworld; + savedDir = "${service.installDir}/Pal/Saved"; + startScript = pkgs.writeShellScript "palworld-start" '' + set -eu + + ${pkgs.coreutils}/bin/install -d \ + -m 0770 \ + ${lib.escapeShellArg service.stateDir} \ + ${lib.escapeShellArg (builtins.dirOf savedDir)} + + if [ -L ${lib.escapeShellArg savedDir} ]; then + current_target="$(${pkgs.coreutils}/bin/readlink ${lib.escapeShellArg savedDir})" + if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then + ${pkgs.coreutils}/bin/rm ${lib.escapeShellArg savedDir} + fi + fi + + if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then + if ${pkgs.findutils}/bin/find ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${pkgs.gnugrep}/bin/grep -q .; then + ${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir} + else + ${pkgs.coreutils}/bin/cp -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/ + ${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir} + fi + fi + + if [ ! -e ${lib.escapeShellArg savedDir} ]; then + ${pkgs.coreutils}/bin/ln -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir} + fi + + password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})" + admin_password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})" + + exec ./PalServer.sh \ + "$@" \ + "-serverpassword=$password" \ + "-adminpassword=$admin_password" + ''; + in + { + modules.services.steamcmd = { + enable = true; + inherit (service) steamDir installDir stateDir; + serviceName = "palworld-${lib.toLower service.serverName}"; + appId = 2394010; + executable = "${startScript}"; + arguments = + [ + "-useperfthreads" + "-NoAsyncLoadingThread" + "-UseMultithreadForDS" + "-port=${toString service.port}" + "-publicport=${toString service.port}" + "-servername=${service.serverName}" + "-players=${toString service.maxPlayers}" + ] + ++ lib.optional service.publicLobby "-publiclobby" + ++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ] + ++ lib.optional service.rcon.enable "-rcon"; + udpPorts = [ service.port ]; + tcpPorts = [ service.port ]; + }; + } + ); +} diff --git a/configs/nixos/modules/services/steamcmd.nix b/configs/nixos/modules/services/steamcmd.nix index 22a67ab..d1203d0 100644 --- a/configs/nixos/modules/services/steamcmd.nix +++ b/configs/nixos/modules/services/steamcmd.nix @@ -59,6 +59,12 @@ default = [ ]; description = "UDP ports opened for the game service."; }; + + tcpPorts = lib.mkOption { + type = lib.types.listOf lib.types.port; + default = [ ]; + description = "TCP ports opened for the game service."; + }; }; config = lib.mkMerge [ @@ -159,6 +165,7 @@ }; networking.firewall.allowedUDPPorts = service.udpPorts; + networking.firewall.allowedTCPPorts = service.tcpPorts; } )) ];