Skip to content

Commit

Permalink
Merge pull request #10 from scottbot95/palworld
Browse files Browse the repository at this point in the history
Add Palworld dedicated server
  • Loading branch information
scottbot95 authored Feb 4, 2024
2 parents d4d4d06 + c351f45 commit 0a022e6
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 32 deletions.
10 changes: 7 additions & 3 deletions apps/updater/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { parseArgs } from 'https://deno.land/[email protected]/cli/parse_args.ts';
import { LockFile, SteamObject, parseObject } from './parser.ts'

const utf8Decoder = new TextDecoder();

/**
* Recursively find all files matching `fileName` under `baseDir`
*
Expand Down Expand Up @@ -31,7 +33,7 @@ const getAppInfo = async (appId: number): Promise<SteamObject> => {
],
});
const app_info_bytes = (await steamcmd.output()).stdout;
const app_info_str = new TextDecoder().decode(app_info_bytes);
const app_info_str = utf8Decoder.decode(app_info_bytes);
const info_start = `"${appId}"`;
const info_start_idx = app_info_str.indexOf(info_start);
const app_info_obj = app_info_str.slice(info_start_idx + info_start.length);
Expand Down Expand Up @@ -140,11 +142,13 @@ async function prefetch(
const output = await command.output();

if (!output.success) {
console.error(new TextDecoder().decode(output.stdout));
console.error(utf8Decoder.decode(output.stdout));
throw new Error(`Prefetching failed with code ${output.code}`);
}

return new TextDecoder().decode(output.stdout);
return utf8Decoder
.decode(output.stdout)
.trim();
}

function usage(): never {
Expand Down
12 changes: 8 additions & 4 deletions apps/updater/prefetch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

set -e

# Redirect all stdout to stderr, but save reference to original stdout
exec 3>&1 >&2

# Enable new nix features
export NIX_CONFIG="experimental-features = nix-command"

Expand All @@ -23,15 +26,16 @@ if [ -n "$debug" ]; then
args+=(-debug)
fi

echo "DepotDownloader ${args[*]} -dir ${downloadDir}" >&2
echo "DepotDownloader ${args[*]} -dir ${downloadDir}"
DepotDownloader \
"${args[@]}" \
-dir "${downloadDir}" >&2
-dir "${downloadDir}"

if [ -n "$addToStore" ]; then
echo "Adding depot to store"
nix store add-path --name "${name:?}" "${downloadDir}" >&2
nix store add-path --name "${name:?}" "${downloadDir}"
fi
nix hash path "${downloadDir}"

nix hash path "${downloadDir}" >&3

rm -rf "${downloadDir}"
35 changes: 12 additions & 23 deletions modules/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ in {

flake.nixosModules.default = {
config,
pkgs,
lib,
...
}:
with lib; let
cfg = config.services.steam-servers;
userHome = config.users.users.${cfg.user}.home;
anyServersEnabled =
any
(conf: conf.enable)
Expand All @@ -44,33 +46,20 @@ in {
inputs.steam-fetcher.overlays.default
];

# Can't use tmpfiles because tmpfiles won't create directories with different owner than parent
systemd.services."make-steam-servers-dir" = let
services =
map
(name: "${name}.service")
(builtins.attrNames cfg.servers);
in {
wantedBy = services;
before = services;

script = ''
mkdir -p ${cfg.datadir}
chown ${cfg.user}:${cfg.group} ${cfg.datadir}
'';

serviceConfig = {
Type = "oneshot";
};
};

users.users.${cfg.user} = {
users.users."${cfg.user}" = {
isSystemUser = true;
home = "${cfg.datadir}";
group = "${cfg.group}";
createHome = true;
homeMode = "750";
inherit (cfg) group;
};

users.groups.${cfg.group} = {};
users.groups."${cfg.group}" = {};

systemd.tmpfiles.rules = [
"d ${userHome}/.steam 0755 ${cfg.user} ${cfg.user} - -"
"L+ ${userHome}/.steam/sdk64 - - - - ${pkgs.steamworks-sdk-redist}/lib"
];
};
};
}
3 changes: 2 additions & 1 deletion modules/lib.nix
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ in {
echo "${n} already exists and isn't a directory, moving"
mv "${n}" "${n}.bak"
fi
${pkgs.rsync}/bin/rsync -avu "${v}" "${n}"
${pkgs.rsync}/bin/rsync -avu "${v}/" "${n}"
chmod -R u+w "${n}"
'')
dirs));
}
101 changes: 101 additions & 0 deletions modules/palworld/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
{
config,
lib,
pkgs,
...
}:
with lib; let
baseCfg = config.services.steam-servers;
cfg = baseCfg.palworld;
enabledServers = filterAttrs (_: conf: conf.enable) cfg;
# settingsFormat = pkgs.formats.ini {};
settingsFormat = {
generate = 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 optVal;
in "${optName}=${encodedVal}")
value;
in
builtins.toFile name ''
[/Script/Pal.PalGameWorldSettings]
OptionSettings=(${concatStringsSep "," optionSettings})
'';
};
in {
imports = [./options.nix];

config = mkIf (enabledServers != {}) {
networking.firewall =
mkMerge
(map
(conf:
mkIf conf.openFirewall {
# allowedUDPPorts = [conf.config.UpdatePort conf.config.GamePort];
allowedUDPPorts = [8211];
})
(builtins.attrValues enabledServers));

services.steam-servers.servers =
mapAttrs'
(name: conf: let
settingsFile = settingsFormat.generate "PalWorldSettings.ini" conf.worldSettings;
in
nameValuePair "palworld-${name}" {
# inherit args;
inherit (conf) enable datadir;

dirs = {
Pal = "${conf.package}/Pal";
Engine = "${conf.package}/Engine";
};

files = {
# Copy start script since it derefernces symlinks to find the server root dir
"PalServer.sh" = "${conf.package}/PalServer.sh";

"Pal/Saved/Config/LinuxServer/PalWorldSettings.ini" = settingsFile;
};

executable = "chmod +x ${conf.datadir}/PalServer.sh; ${pkgs.steam-run}/bin/steam-run ${conf.datadir}/PalServer.sh";

args =
[
"-port=${toString conf.port}"
"-useperfthreads"
"-NoAsyncLoadingThread"
"-UseMultithreadForDS"
]
++ conf.extraArgs;
})
cfg;

systemd.services =
mapAttrs'
(
name: _conf:
nameValuePair "palworld-${name}" {
path = with pkgs; [
xdg-user-dirs
];

serviceConfig = {
# Palworld needs namespaces and system calls
RestrictNamespaces = false;
SystemCallFilter = [];
};
}
)
enabledServers;
};
}
23 changes: 23 additions & 0 deletions modules/palworld/default.test.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{lib, ...}:
with lib; {
name = "palworld";

nodes = {
server = {
virtualisation = {
cores = 8;
memorySize = 16 * 1024;
diskSize = 8 * 1024;
};

services.steam-servers.palworld.test = {
enable = true;
openFirewall = true;
};
};
};

testScript = ''
server.wait_for_unit("palworld-test.service")
'';
}
62 changes: 62 additions & 0 deletions modules/palworld/options.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
config,
lib,
pkgs,
...
}:
with lib; let
baseCfg = config.services.steam-servers;
moduleLib = import ../lib.nix lib;
inherit (moduleLib) mkOpt;

serverModule = {name, ...}: {
options = {
enable = mkEnableOption (mdDoc "Palworld Dedicated Server");

package = mkOption {
type = types.package;
default = pkgs.palworld;
defaultText = literalExpression "pkgs.palworld";
description = mdDoc "Package to use for Palworld binary";
};

datadir = mkOption {
type = types.path;
default = "${baseCfg.datadir}/palworld/${name}";
defaultText = literalExpression "\${services.steam-servers.datadir}/palworld/\${name}";
description = mdDoc ''
Directory to store save state of the game server. (eg world, saves, etc)
'';
};

openFirewall = mkOption {
type = types.bool;
default = false;
description = mdDoc "Whether to open ports in the firewall.";
};

port = mkOption {
type = types.port;
default = 8211;
description = mdDoc "UDP port to listen on";
};

worldSettings = mkOption {
# inherit (settingsFormat) type;
type = types.attrs;
default = {};
description = mdDoc "World settings used to generate PalWorldSettings.ini";
};

extraArgs = mkOpt (with types; listOf str) [] "Extra command line arguments to pass to the server";
};
};
in {
options.services.steam-servers.palworld = mkOption {
type = types.attrsOf (types.submodule serverModule);
default = {};
description = mdDoc ''
Options to configure one or more Stationers servers.
'';
};
}
11 changes: 10 additions & 1 deletion modules/servers/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,23 @@ in {
User = mkDefault "${baseCfg.user}";
Group = mkDefault "${baseCfg.group}";

RuntimeDirectory = mkDefault "steam-servers";
RuntimeDirectoryPreserve = mkDefault true;

# These don't use mkDefault as they are inherent to how this module works
# Type = "forking";
# GuessMainPID = true;

PrivateDevices = mkDefault true;
PrivateTmp = mkDefault true;
PrivateUsers = mkDefault true;
ProtectClock = mkDefault true;
ProtectProc = mkDefault "noaccess";
ProtectKernelLogs = mkDefault true;
ProtectKernelModules = mkDefault true;
ProtectKernelTunables = mkDefault true;
ProtectControlGroups = mkDefault true;
ProtectHostname = mkDefault true;
PrivateDevices = mkDefault true;
RestrictRealtime = mkDefault true;
RestrictNamespaces = mkDefault true;
LockPersonality = mkDefault true;
Expand Down
1 change: 1 addition & 0 deletions pkgs/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
}: let
pkgsToImport = {
"7-days-to-die" = ./7-days-to-die;
palworld = ./palworld;
stationeers = ./stationeers;
};

Expand Down
23 changes: 23 additions & 0 deletions pkgs/palworld/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
lib,
mkSteamPackage,
gcc-unwrapped,
}:
mkSteamPackage {
lockFile = ./lock.json;

buildInputs = [
gcc-unwrapped
];

meta = with lib; {
description = "Palworld Dedicated Server";
homepage = "https://steamdb.info/app/2394010/";
changelog = "https://store.steampowered.com/news/app/1623730?updates=true";
sourceProvenance = with sourceTypes; [
binaryNativeCode # Steam games are always going to contain some native binary component.
];
license = licenses.unfree;
platforms = ["x86_64-linux"];
};
}
Loading

0 comments on commit 0a022e6

Please sign in to comment.