Render Palworld secrets through its documented settings file and remove stale RCON configuration. Harden generated commands, centralize shared modules, and codify the reviewed Nix conventions.
198 lines
6.8 KiB
Nix
198 lines
6.8 KiB
Nix
{
|
|
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.";
|
|
};
|
|
|
|
autoStart = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Start the Palworld server after the guest reaches network readiness.";
|
|
};
|
|
|
|
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";
|
|
defaultSettingsFile = "${service.installDir}/DefaultPalWorldSettings.ini";
|
|
settingsFile = "${savedDir}/Config/LinuxServer/PalWorldSettings.ini";
|
|
renderSettingsScript = pkgs.writeText "render-palworld-settings.py" ''
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
|
|
default_path, settings_path, server_name, password_path, admin_password_path = sys.argv[1:]
|
|
default_path = Path(default_path)
|
|
settings_path = Path(settings_path)
|
|
|
|
source_path = settings_path if settings_path.exists() else default_path
|
|
text = source_path.read_text(encoding="utf-8")
|
|
|
|
def read_secret(path):
|
|
return Path(path).read_text(encoding="utf-8").rstrip("\r\n")
|
|
|
|
def set_string_setting(text, name, value):
|
|
assignment = f"{name}={json.dumps(value, ensure_ascii=False)}"
|
|
pattern = re.compile(rf'(?<![A-Za-z0-9_]){re.escape(name)}="(?:[^"\\]|\\.)*"')
|
|
text, replacements = pattern.subn(lambda _match: assignment, text, count=1)
|
|
if replacements:
|
|
return text
|
|
|
|
marker = "OptionSettings=("
|
|
if marker not in text:
|
|
raise ValueError(f"{source_path} has no OptionSettings declaration")
|
|
return text.replace(marker, f"{marker}{assignment},", 1)
|
|
|
|
settings = {
|
|
"ServerName": server_name,
|
|
"ServerPassword": read_secret(password_path),
|
|
"AdminPassword": read_secret(admin_password_path),
|
|
}
|
|
for name, value in settings.items():
|
|
text = set_string_setting(text, name, value)
|
|
|
|
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
descriptor, temporary_name = tempfile.mkstemp(
|
|
prefix=f".{settings_path.name}.",
|
|
dir=settings_path.parent,
|
|
)
|
|
temporary_path = Path(temporary_name)
|
|
try:
|
|
os.fchmod(descriptor, 0o600)
|
|
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as settings_file:
|
|
settings_file.write(text)
|
|
os.replace(temporary_path, settings_path)
|
|
finally:
|
|
temporary_path.unlink(missing_ok=True)
|
|
'';
|
|
startScript = pkgs.writeShellScript "palworld-start" ''
|
|
set -eu
|
|
|
|
${lib.getExe' pkgs.coreutils "install"} -d \
|
|
-m 0770 \
|
|
-- \
|
|
${lib.escapeShellArg service.stateDir} \
|
|
${lib.escapeShellArg (builtins.dirOf savedDir)}
|
|
|
|
if [ -L ${lib.escapeShellArg savedDir} ]; then
|
|
current_target="$(${lib.getExe' pkgs.coreutils "readlink"} -- ${lib.escapeShellArg savedDir})"
|
|
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
|
|
${lib.getExe' pkgs.coreutils "rm"} -- ${lib.escapeShellArg savedDir}
|
|
fi
|
|
fi
|
|
|
|
if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then
|
|
if ${lib.getExe' pkgs.findutils "find"} -- ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${lib.getExe pkgs.gnugrep} -q .; then
|
|
${lib.getExe' pkgs.coreutils "rm"} -rf -- ${lib.escapeShellArg savedDir}
|
|
else
|
|
${lib.getExe' pkgs.coreutils "cp"} -a -- ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
|
|
${lib.getExe' pkgs.coreutils "rm"} -rf -- ${lib.escapeShellArg savedDir}
|
|
fi
|
|
fi
|
|
|
|
if [ ! -e ${lib.escapeShellArg savedDir} ]; then
|
|
${lib.getExe' pkgs.coreutils "ln"} -s -- ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
|
|
fi
|
|
|
|
${lib.getExe pkgs.python3} \
|
|
${renderSettingsScript} \
|
|
${lib.escapeShellArg defaultSettingsFile} \
|
|
${lib.escapeShellArg settingsFile} \
|
|
${lib.escapeShellArg service.serverName} \
|
|
${lib.escapeShellArg service.passwordFile} \
|
|
${lib.escapeShellArg service.adminPasswordFile}
|
|
|
|
exec ./PalServer.sh "$@"
|
|
'';
|
|
in
|
|
{
|
|
modules.services.steamcmd = {
|
|
enable = true;
|
|
inherit (service) steamDir installDir stateDir;
|
|
serviceName = "palworld";
|
|
appId = 2394010;
|
|
inherit (service) autoStart;
|
|
executable = "${startScript}";
|
|
arguments = [
|
|
"-port=${toString service.port}"
|
|
"-players=${toString service.maxPlayers}"
|
|
]
|
|
++ lib.optionals service.publicLobby (
|
|
[
|
|
"-publiclobby"
|
|
"-publicport=${toString service.port}"
|
|
]
|
|
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
|
|
);
|
|
udpPorts = [ service.port ];
|
|
};
|
|
}
|
|
);
|
|
}
|