{ 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 ]; }; } ); }