{ config, lib, pkgs, ... }: { options.modules.services.steamcmd = { enable = lib.mkEnableOption "SteamCMD-managed game server"; steamDir = lib.mkOption { type = lib.types.str; example = "/data/steamcmd"; description = "Directory where SteamCMD stores metadata and cache files."; }; serviceName = lib.mkOption { type = lib.types.strMatching "[a-z0-9][a-z0-9-]*"; example = "valheim"; description = "Lowercase systemd service basename for the game server."; }; appId = lib.mkOption { type = lib.types.ints.positive; example = 896660; description = "Steam app ID for the dedicated server package."; }; installDir = lib.mkOption { type = lib.types.str; example = "/data/servers/valheim/main"; description = "Directory where SteamCMD installs the server files and where the game service starts."; }; stateDir = lib.mkOption { type = lib.types.str; example = "/data/state/valheim/main"; description = "Writable directory for durable game server state."; }; executable = lib.mkOption { type = lib.types.str; example = "./valheim_server.x86_64"; description = "Server executable, relative to installDir or absolute."; }; arguments = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; example = [ "-name" "main" ]; description = "Arguments passed to the server executable."; }; udpPorts = lib.mkOption { type = lib.types.listOf lib.types.port; default = [ ]; description = "UDP ports opened for the game service."; }; tcpPorts = lib.mkOption { type = lib.types.listOf lib.types.port; default = [ ]; description = "TCP ports opened for the game service."; }; autoStart = lib.mkOption { type = lib.types.bool; default = true; description = "Start the game service after the guest reaches network readiness."; }; }; config = lib.mkMerge [ { registry."stable-uid".${config.networking.hostName}.steamcmd = 20003; } (lib.mkIf config.modules.services.steamcmd.enable ( let group = "storage"; service = config.modules.services.steamcmd; user = "steamcmd"; prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" '' set -eu ${lib.getExe' pkgs.coreutils "install"} -d \ -o ${user} \ -g ${group} \ -m 0770 \ ${lib.escapeShellArg service.steamDir} \ ${lib.escapeShellArg service.installDir} \ ${lib.escapeShellArg service.stateDir} ''; updateScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-update" '' set -eu export HOME=${lib.escapeShellArg service.steamDir} exec ${lib.getExe pkgs.steamcmd} \ +force_install_dir ${lib.escapeShellArg service.installDir} \ +login anonymous \ +app_update ${toString service.appId} validate \ +quit ''; startScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-start" '' set -eu cd ${lib.escapeShellArg service.installDir} export LD_LIBRARY_PATH=linux64:''${LD_LIBRARY_PATH-} exec ${lib.getExe pkgs.steam-run} ${lib.escapeShellArg service.executable} ${lib.escapeShellArgs service.arguments} ''; in { users.users.${user} = { isSystemUser = true; uid = config.registry."stable-uid".${config.networking.hostName}.steamcmd; inherit group; home = service.steamDir; }; systemd.tmpfiles.rules = [ "d ${service.steamDir} 0770 ${user} ${group} - -" "Z ${service.steamDir} 0770 ${user} ${group} - -" "d ${service.installDir} 0770 ${user} ${group} - -" "Z ${service.installDir} 0770 ${user} ${group} - -" "d ${service.stateDir} 0770 ${user} ${group} - -" "Z ${service.stateDir} 0770 ${user} ${group} - -" ]; systemd.services = { "steamcmd-${service.serviceName}-update" = { description = "Update ${service.serviceName} with SteamCMD"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; serviceConfig = { Type = "oneshot"; User = user; Group = group; WorkingDirectory = "-${service.steamDir}"; ExecStartPre = "+${prepareScript}"; ExecStart = updateScript; TimeoutStartSec = "15min"; }; }; "${service.serviceName}-autostart" = { description = "Queue ${service.serviceName} game server startup"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = lib.mkIf service.autoStart [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; ExecStart = "${lib.getExe' pkgs.systemd "systemctl"} --no-block start ${service.serviceName}.service"; }; }; ${service.serviceName} = { description = "${service.serviceName} game server"; after = [ "network-online.target" "steamcmd-${service.serviceName}-update.service" ]; wants = [ "network-online.target" "steamcmd-${service.serviceName}-update.service" ]; serviceConfig = { Type = "simple"; User = user; Group = group; WorkingDirectory = "-${service.installDir}"; ExecStartPre = "+${prepareScript}"; ExecStart = startScript; Restart = "on-failure"; TimeoutStartSec = "15min"; }; }; }; networking.firewall.allowedUDPPorts = service.udpPorts; networking.firewall.allowedTCPPorts = service.tcpPorts; } )) ]; }