{ lib, pkgs, nixpkgs-vintagestory, ... }: let # Take vintagestory (1.22.3) from our rev-pinned nixpkgs input so the # server version only changes when we deliberately bump the rev in # flake.nix — clients must match the server version to connect. vsPkgs = import nixpkgs-vintagestory { system = "x86_64-linux"; config.allowUnfree = true; }; vintagestory = vsPkgs.vintagestory; mods = map (m: pkgs.fetchurl { inherit (m) name url hash; }) (import ./vintagestory-mods.nix); modDir = pkgs.linkFarm "vintagestory-mods" ( map (drv: { inherit (drv) name; path = drv; }) mods ); # World settings only apply when the world is first created — the server # bakes them into the save file, so editing these later does nothing for # an existing world (delete the save to regenerate). # # The seeded serverconfig.json must be COMPLETE, not just a WorldConfig # fragment: the server only generates its default Roles/etc. when no file # exists at all, so a partial file leaves DefaultRoleCode pointing at a # role that isn't defined and the server kills itself on start. So we # start from the canonical default (captured from this exact server # version into vintagestory-serverconfig.json) and merge our settings in. worldConfiguration = builtins.fromJSON (builtins.readFile ./vintagestory-worldconfig.json); baseServerConfig = builtins.fromJSON (builtins.readFile ./vintagestory-serverconfig.json); serverConfig = lib.recursiveUpdate baseServerConfig { MapSizeY = worldConfiguration.worldHeight; WorldConfig = { Seed = "534793158"; WorldName = "ellie.town"; PlayStyle = worldConfiguration.playstyle; MapSizeY = worldConfiguration.worldHeight; SaveFileLocation = "/var/lib/vintagestory/Saves/ellie.town.vcdbs"; WorldConfiguration = worldConfiguration; }; }; initialServerConfig = pkgs.writeText "vintagestory-serverconfig.json" ( builtins.toJSON serverConfig ); # Access policy, re-asserted on every start (see preStart). Unlike world # settings, these must apply to the already-created world, and the server # owns serverconfig.json at runtime — so we merge just these keys into the # live file each start rather than seeding them once. # WhitelistMode: 0 = default (on for dedicated servers), 1 = off, 2 = on serverPolicy = pkgs.writeText "vintagestory-policy.json" ( builtins.toJSON { WhitelistMode = 1; Password = "noxalia"; } ); in { # Friends connect directly, so accept game traffic on all interfaces networking.firewall.allowedTCPPorts = [ 42420 ]; networking.firewall.allowedUDPPorts = [ 42420 ]; users.users.vintagestory = { isSystemUser = true; group = "vintagestory"; home = "/var/lib/vintagestory"; }; users.groups.vintagestory = { }; systemd.services.vintagestory = { description = "Vintage Story dedicated server"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; environment = { # dotnet wants a writable HOME for its temp/config files HOME = "/var/lib/vintagestory"; }; # The server rewrites serverconfig.json at runtime (it's mutable state), # so seed the full config once on first start, then re-assert just our # access-policy keys on every start so repo changes reach an existing # world. World-creation settings in the seed are deliberately NOT # re-asserted — they only matter at creation and the save owns them now. preStart = '' cfg=/var/lib/vintagestory/serverconfig.json if [ ! -e "$cfg" ]; then install -m 0600 ${initialServerConfig} "$cfg" fi tmp=$(mktemp -p /var/lib/vintagestory) # Only overwrite the live config if the merge actually succeeds, so a # jq error can never truncate serverconfig.json to an empty file. if ${pkgs.jq}/bin/jq -s '.[0] * .[1]' "$cfg" ${serverPolicy} > "$tmp"; then install -m 0600 "$tmp" "$cfg" fi rm -f "$tmp" ''; serviceConfig = { ExecStart = "${vintagestory}/bin/vintagestory-server --dataPath /var/lib/vintagestory --addModPath ${modDir}"; User = "vintagestory"; Group = "vintagestory"; StateDirectory = "vintagestory"; StateDirectoryMode = "0700"; WorkingDirectory = "/var/lib/vintagestory"; Restart = "on-failure"; RestartSec = "10s"; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; NoNewPrivileges = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictSUIDSGID = true; LockPersonality = true; }; }; }