Personal Nix setup

Add Palworld server

+4
machines/ramune/configuration.nix
···
caddy.enable = true;
vaultwarden.enable = true;
};
+
games = {
+
enable = true;
+
palworld.enable = true;
+
};
};
system.stateVersion = "24.11";
+1
modules/default.nix
···
./apps
./desktop
./fonts
+
./games
./nvim
./router
./server
+54
modules/games/default.nix
···
+
{ lib, config, ... }:
+
+
with lib; let
+
cfg = config.modules.games;
+
in {
+
options.modules.games = {
+
enable = mkOption {
+
default = false;
+
example = true;
+
description = "Whether to enable game server options.";
+
type = types.bool;
+
};
+
+
datadir = mkOption {
+
type = types.path;
+
default = "/var/lib/games";
+
description = "Base directory for all game servers created with this module.";
+
example = "/mnt/nfs/steam";
+
};
+
+
user = mkOption {
+
type = types.str;
+
default = "games";
+
description = "User to use when running game servers and creating top-level resources";
+
};
+
+
group = mkOption {
+
type = types.str;
+
default = cfg.user;
+
defaultText = literalExpression "\${cfg.user}";
+
description = "Group to use when running game servers";
+
};
+
};
+
+
config = mkIf cfg.enable {
+
users.users."${cfg.user}" = {
+
home = "${cfg.datadir}";
+
group = cfg.group;
+
isSystemUser = true;
+
createHome = true;
+
homeMode = "750";
+
};
+
+
users.groups."${cfg.group}" = {};
+
+
systemd.tmpfiles.rules = [
+
"d ${cfg.datadir}/.steam 0755 ${cfg.user} ${cfg.group} - -"
+
];
+
};
+
+
imports = [
+
./palworld.nix
+
];
+
}
+53
modules/games/lib/fetchSteam.nix
···
+
{ lib, pkgs, ... }:
+
+
with lib; makeOverridable (
+
{
+
name ? "steamapp-${appId}-${depotId}-${manifestId}",
+
appId,
+
depotId,
+
manifestId,
+
hash ? "",
+
branch ? null,
+
fileList ? null,
+
debug ? false,
+
passthru ? { },
+
meta ? { },
+
} @ args:
+
let
+
fileListArg =
+
if isList fileList then
+
builtins.toFile "steam-files-list.txt" (concatLines fileList)
+
else
+
fileList;
+
+
downloadArgs =
+
[
+
"-app" appId
+
"-depot" depotId
+
"-manifest" manifestId
+
]
+
++ optionals (branch != null) [ "-beta" branch ]
+
++ optionals (fileList != null) [ "-filelist" fileListArg ]
+
++ optionals debug [ "-debug" ];
+
+
drvArgs = {
+
depsBuildBuild = [ pkgs.depotdownloader ];
+
+
strictDeps = true;
+
+
outputHashAlgo = "sha256";
+
outputHashMode = "recursive";
+
outputHash = if hash != "" then hash else fakeHash;
+
+
env.SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
+
+
pos = builtins.unsafeGetAttrPos "manifestId" args;
+
+
inherit passthru;
+
} // optionalAttrs (args ? meta) { inherit meta; };
+
in
+
pkgs.runCommand name drvArgs ''
+
HOME=$PWD DepotDownloader -dir "$out" ${escapeShellArgs downloadArgs}
+
rm -r "$out"/.DepotDownloader
+
''
+
)
+35
modules/games/lib/mkSteamPackage.nix
···
+
{ lib, pkgs, ... } @ inputs:
+
+
with lib;
+
let
+
fetchSteam = (import ./fetchSteam.nix) inputs;
+
inherit ((import ./steamworks.nix) inputs) steamworks-sdk-redist;
+
in
+
{
+
name,
+
hash ? "",
+
version,
+
appId,
+
depotId,
+
manifestId,
+
...
+
} @ args: let
+
mkDerivationArgs = builtins.removeAttrs args [ "name" "appId" "depotId" "manifestId" "hash" ];
+
in pkgs.stdenv.mkDerivation (rec {
+
pname = name;
+
src = fetchSteam {
+
inherit name appId depotId manifestId hash;
+
};
+
dontBuild = true;
+
dontConfigure = true;
+
dontFixup = true;
+
installPhase = ''
+
runHook preInstall
+
+
mkdir -p $out
+
mv ./* $out
+
chmod 755 -R $out
+
+
runHook postInstall
+
'';
+
} // mkDerivationArgs)
+52
modules/games/lib/mkWrappedBox64.nix
···
+
{ lib, pkgs, ... } @ inputs:
+
+
with lib;
+
let
+
inherit ((import ./steamworks.nix) inputs) steamworks-sdk-redist;
+
in {
+
logLevel ? 0,
+
env ? {},
+
libs ? [],
+
extraWrapperArgs ? [],
+
}: let
+
box64Bin = "${pkgs.box64}/bin/box64";
+
runpaths = with pkgs; [
+
steamworks-sdk-redist
+
glibc
+
libxcrypt
+
libGL
+
libdrm
+
mesa # for libgbm
+
udev
+
libudev0-shim
+
libva
+
vulkan-loader
+
];
+
combinedEnv = {
+
BOX64_DYNAREC_STRONGMEM = 1;
+
BOX64_DYNAREC_BIGBLOCK = 1;
+
BOX64_DYNAREC_SAFEFLAGS = 1;
+
BOX64_DYNAREC_FASTROUND = 1;
+
BOX64_DYNAREC_FASTNAN = 1;
+
BOX64_DYNAREC_X87DOUBLE = 0;
+
} // env;
+
in pkgs.stdenv.mkDerivation rec {
+
name = "box64-wrapped";
+
+
dontUnpack = true;
+
dontConfigure = true;
+
dontBuild = true;
+
+
nativeBuildInputs = [ pkgs.makeWrapper ];
+
buildInputs = runpaths ++ libs;
+
+
installPhase = ''
+
runHook preInstall
+
makeWrapper "${box64Bin}" "$out/bin/box64" \
+
${concatStrings (mapAttrsToList (name: value: "--set ${name} '${toString value}' ") env)} \
+
--set BOX64_LOG "${toString logLevel}" \
+
--prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath buildInputs} \
+
${lib.strings.concatStringsSep " " extraWrapperArgs}
+
runHook postInstall
+
'';
+
}
+73
modules/games/lib/serverScripts.nix
···
+
{ lib, pkgs, ... }:
+
+
with lib;
+
{
+
mkSymlinks = name: symlinks:
+
pkgs.writeShellScript "${name}-symlinks"
+
(concatStringsSep "\n"
+
(mapAttrsToList
+
(n: v: ''
+
if [[ -L "${n}" ]]; then
+
unlink "${n}"
+
elif [[ -e "${n}" ]]; then
+
echo "${n} already exists, moving"
+
mv "${n}" "${n}.bak"
+
fi
+
mkdir -p "$(dirname "${n}")"
+
ln -sf "${v}" "${n}"
+
'')
+
symlinks));
+
+
mkFiles = name: files:
+
pkgs.writeShellScript "${name}-files"
+
(concatStringsSep "\n"
+
(mapAttrsToList
+
(n: v: ''
+
if [[ -L "${n}" ]]; then
+
unlink "${n}"
+
elif ${pkgs.diffutils}/bin/cmp -s "${n}" "${v}"; then
+
rm "${n}"
+
elif [[ -e "${n}" ]]; then
+
echo "${n} already exists, moving"
+
mv "${n}" "${n}.bak"
+
fi
+
mkdir -p $(dirname "${n}")
+
${pkgs.gawk}/bin/awk '{
+
for(varname in ENVIRON)
+
gsub("@"varname"@", ENVIRON[varname])
+
print
+
}' "${v}" > "${n}"
+
chmod --reference="${v}" "${n}"
+
'')
+
files));
+
+
mkDirs = name: dirs:
+
pkgs.writeShellScript "${name}-dirs"
+
(concatStringsSep "\n"
+
(mapAttrsToList
+
(n: v: ''
+
if [[ -L "${n}" ]]; then
+
unlink "${n}"
+
elif [[ ! -d "${n}" ]]; then
+
echo "${n} already exists and isn't a directory, moving"
+
mv "${n}" "${n}.bak"
+
fi
+
${pkgs.rsync}/bin/rsync -avu "${v}/" "${n}"
+
chmod -R u+w "${n}"
+
'')
+
dirs));
+
+
rmSymlinks = name: symlinks:
+
pkgs.writeShellScript "${name}-rm-symlinks"
+
(
+
concatStringsSep "\n"
+
(mapAttrsToList (n: _v: "unlink \"${n}\"") symlinks)
+
);
+
+
rmFiles = name: files:
+
pkgs.writeShellScript "${name}-rm-symlinks"
+
(
+
concatStringsSep "\n"
+
(mapAttrsToList (n: _v: "rm \"${n}\"") files)
+
);
+
}
+42
modules/games/lib/steamworks.nix
···
+
{ lib, pkgs, ... } @ inputs:
+
+
with lib;
+
let
+
fetchSteam = (import ./fetchSteam.nix) inputs;
+
in {
+
steamworks-sdk-redist = pkgs.stdenv.mkDerivation {
+
pname = "steamworks-sdk-redist";
+
version = "unstable-2024-05-30";
+
+
# Steamworks SDK Redist with steamclient.so.
+
# https://steamdb.info/app/1007/depots
+
src = fetchSteam {
+
appId = "1007";
+
depotId = "1006";
+
manifestId = "7138471031118904166";
+
hash = "sha256-OtPI1kAx6+9G09IEr2kYchyvxlPl3rzx/ai/xEVG4oM=";
+
};
+
+
dontConfigure = true;
+
dontBuild = true;
+
+
installPhase = ''
+
runHook preInstall
+
+
mkdir -p $out/lib
+
cp linux64/steamclient.so $out/lib
+
chmod +x $out/lib/steamclient.so
+
+
runHook postInstall
+
'';
+
+
meta = {
+
description = "Steamworks SDK Redist";
+
sourceProvenance = [ sourceTypes.binaryNativeCode ];
+
license = licenses.unfree;
+
badPlatforms = [
+
{ hasSharedLibraries = false; }
+
];
+
};
+
};
+
}
+189
modules/games/palworld.nix
···
+
{ lib, config, pkgs, ... } @ args:
+
+
with lib;
+
let
+
isEnabled = config.modules.games.enable && config.modules.games.palworld.enable;
+
baseCfg = config.modules.games;
+
cfg = config.modules.games.palworld;
+
+
name = "palworld-server";
+
serverScripts = (import ./lib/serverScripts.nix) args;
+
mkWrappedBox64 = (import ./lib/mkWrappedBox64.nix) args;
+
mkSteamPackage = (import ./lib/mkSteamPackage.nix) args;
+
inherit ((import ./lib/steamworks.nix) args) steamworks-sdk-redist;
+
+
generateSettings = name: value: let
+
optionSettings =
+
mapAttrsToList
+
(optName: optVal: let
+
optType = builtins.typeOf optVal;
+
encodedVal =
+
if optType == "string"
+
then "\"${optVal}\""
+
else if optType == "bool"
+
then
+
if optVal
+
then "True"
+
else "False"
+
else toString optVal;
+
in "${optName}=${encodedVal}")
+
value;
+
in
+
builtins.toFile name ''
+
[/Script/Pal.PalGameWorldSettings]
+
OptionSettings=(${concatStringsSep "," optionSettings})
+
'';
+
+
wrappedBox64 = mkWrappedBox64 {
+
libs = [ pkgs.pkgsCross.gnu64.libgcc ];
+
};
+
+
palworld-server = mkSteamPackage {
+
name = "palworld-server";
+
version = "17082920";
+
appId = "2394010";
+
depotId = "2394012";
+
manifestId = "2423583208459052375";
+
hash = "sha256-gAFEDf/rKPQ5zTH8EJ93e4KKHUGi8uiYlPS7G2lWGWk=";
+
meta = {
+
description = "Palworld Dedicated Server";
+
homepage = "https://steamdb.info/app/2394010/";
+
changelog = "https://store.steampowered.com/news/app/1623730?updates=true";
+
sourceProvenance = with sourceTypes; [ sourceTypes.binaryNativeCode ];
+
};
+
};
+
+
baseSettings = {
+
ServerName = "London Boroughs";
+
AllowConnectPlatform = "Xbox";
+
CoopPlayerMaxNum = cfg.maxPlayers;
+
bIsUseBackupSaveData = true;
+
RCONEnabled = false;
+
RESTAPIEnabled = false;
+
};
+
in
+
{
+
options.modules.games.palworld = {
+
enable = mkOption {
+
default = false;
+
description = "Whether to enable Palworld Dedicated Server.";
+
type = types.bool;
+
};
+
+
package = mkOption {
+
type = types.package;
+
default = palworld-server;
+
};
+
+
autostart = mkOption {
+
default = false;
+
type = types.bool;
+
};
+
+
datadir = mkOption {
+
type = types.path;
+
default = "${baseCfg.datadir}/palworld";
+
};
+
+
ip = mkOption {
+
type = types.nullOr types.str;
+
default = "0.0.0.0";
+
};
+
+
port = mkOption {
+
type = types.port;
+
default = 8211;
+
};
+
+
threads = mkOption {
+
type = types.int;
+
default = 4;
+
};
+
+
maxPlayers = mkOption {
+
type = types.int;
+
default = 6;
+
};
+
+
settings = mkOption {
+
type = types.attrs;
+
default = {
+
PublicPort = 8211;
+
PublicIP = cfg.ip;
+
AllowConnectPlatform = "Xbox";
+
};
+
};
+
};
+
+
config = mkIf isEnabled {
+
modules.router.nftables.capturePorts = [ cfg.port ];
+
networking.firewall.allowedUDPPorts = [ cfg.port ];
+
+
systemd.tmpfiles.rules = [
+
"d ${cfg.datadir} 0755 ${baseCfg.user} ${baseCfg.group} - -"
+
];
+
+
systemd.services."${name}" = let
+
dirs = {
+
Pal = "${cfg.package}/Pal";
+
Engine = "${cfg.package}/Engine";
+
};
+
+
files = let
+
settings = baseSettings // cfg.settings;
+
in {
+
"Pal/Binaries/Linux/steamclient.so" = "${steamworks-sdk-redist}/lib/steamclient.so";
+
"Pal/Saved/Config/LinuxServer/PalWorldSettings.ini" = generateSettings "PalWorldSettings.ini" settings;
+
};
+
+
script = let
+
args = [
+
"Pal"
+
"-port=${toString cfg.port}"
+
"-useperfthreads"
+
"-NoAsyncLoadingThread"
+
"-UseMultithreadForDS"
+
"-players=${toString cfg.maxPlayers}"
+
"-NumberOfWorkerThreadsServer=${toString cfg.threads}"
+
] ++ optionals (cfg.ip != null) [ "-publicip=${cfg.ip}" ];
+
executable = "${cfg.datadir}/Pal/Binaries/Linux/PalServer-Linux-Shipping";
+
command = "${wrappedBox64}/bin/box64 ${executable}";
+
in "${command} ${concatStringsSep " " args}";
+
in {
+
wantedBy = mkIf cfg.autostart [ "multi-user.target" ];
+
after = [ "network.target" ];
+
path = with pkgs; [ xdg-user-dirs util-linux ];
+
+
inherit script;
+
preStart = ''
+
${serverScripts.mkDirs name dirs}
+
${serverScripts.mkFiles name files}
+
'';
+
+
serviceConfig = {
+
Restart = "on-failure";
+
User = "${baseCfg.user}";
+
Group = "${baseCfg.group}";
+
WorkingDirectory = "${cfg.datadir}";
+
+
CPUWeight = 80;
+
CPUQuota = "${toString ((cfg.threads + 1) * 100)}%";
+
+
PrivateDevices = true;
+
PrivateTmp = true;
+
PrivateUsers = true;
+
ProtectClock = true;
+
ProtectProc = "noaccess";
+
ProtectKernelLogs = true;
+
ProtectKernelModules = true;
+
ProtectKernelTunables = true;
+
RestrictRealtime = true;
+
LockPersonality = true;
+
+
# Palworld needs namespaces and system calls
+
RestrictNamespaces = false;
+
SystemCallFilter = [];
+
};
+
};
+
};
+
}