{ config, lib, pkgs, ... }: { options.modules.services.steamcmd = { enable = lib.mkEnableOption "SteamCMD-managed game server"; package = lib.mkPackageOption pkgs "steamcmd" { }; serviceName = lib.mkOption { type = lib.types.str; default = "steamcmd"; example = "valheim"; description = "Name of the systemd service that runs the game server."; }; user = lib.mkOption { type = lib.types.str; default = "steamcmd"; description = "User account that runs SteamCMD and the game server."; }; group = lib.mkOption { type = lib.types.str; default = "storage"; description = "Group account that owns writable server files."; }; steamDir = lib.mkOption { type = lib.types.str; default = "/data/steamcmd"; description = "Directory where SteamCMD stores metadata and cache files."; }; 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."; }; libraryDirs = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "linux64" ]; description = "Library directories added to LD_LIBRARY_PATH before starting the server."; }; environmentFiles = lib.mkOption { type = lib.types.listOf lib.types.path; default = [ ]; description = "Environment files loaded by the game service."; }; updateOnStart = lib.mkOption { type = lib.types.bool; default = true; description = "Runs SteamCMD before each game service start."; }; validateUpdate = lib.mkOption { type = lib.types.bool; default = true; description = "Passes validate to SteamCMD app_update."; }; tcpPorts = lib.mkOption { type = lib.types.listOf lib.types.port; default = [ ]; description = "TCP ports opened for the game service."; }; udpPorts = lib.mkOption { type = lib.types.listOf lib.types.port; default = [ ]; description = "UDP ports opened for the game service."; }; restart = { enable = lib.mkOption { type = lib.types.bool; default = true; description = "Enables the scheduled restart timer."; }; onCalendar = lib.mkOption { type = lib.types.str; default = "daily"; example = "*-*-* 05:00:00"; description = "Calendar expression for the restart timer."; }; randomizedDelaySec = lib.mkOption { type = lib.types.str; default = "15m"; description = "Maximum randomized delay for the restart timer."; }; command = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "systemctl restart valheim.service"; description = "Command run by the restart timer."; }; }; }; config = lib.mkMerge [ { registry."stable-uid".${config.networking.hostName}.steamcmd = 20003; } (lib.mkIf config.modules.services.steamcmd.enable ( let prepareScript = pkgs.writeShellScript "steamcmd-${config.modules.services.steamcmd.serviceName}-prepare" '' set -eu ${pkgs.coreutils}/bin/install -d \ -o ${lib.escapeShellArg config.modules.services.steamcmd.user} \ -g ${lib.escapeShellArg config.modules.services.steamcmd.group} \ -m 0770 \ ${lib.escapeShellArg config.modules.services.steamcmd.steamDir} \ ${lib.escapeShellArg config.modules.services.steamcmd.installDir} \ ${lib.escapeShellArg config.modules.services.steamcmd.stateDir} ''; updateScript = pkgs.writeShellScript "steamcmd-${config.modules.services.steamcmd.serviceName}-update" '' set -eu export HOME=${lib.escapeShellArg config.modules.services.steamcmd.steamDir} exec ${lib.getExe config.modules.services.steamcmd.package} \ +force_install_dir ${lib.escapeShellArg config.modules.services.steamcmd.installDir} \ +login anonymous \ +app_update ${toString config.modules.services.steamcmd.appId} ${lib.optionalString config.modules.services.steamcmd.validateUpdate "validate"} \ +quit ''; libraryPath = lib.concatStringsSep ":" config.modules.services.steamcmd.libraryDirs; startScript = pkgs.writeShellScript "steamcmd-${config.modules.services.steamcmd.serviceName}-start" '' set -eu cd ${lib.escapeShellArg config.modules.services.steamcmd.installDir} ${lib.optionalString (config.modules.services.steamcmd.libraryDirs != [ ]) ''export LD_LIBRARY_PATH=${lib.escapeShellArg libraryPath}:''${LD_LIBRARY_PATH-}''} exec ${lib.getExe pkgs.steam-run} ${lib.escapeShellArg config.modules.services.steamcmd.executable} ${lib.escapeShellArgs config.modules.services.steamcmd.arguments} ''; restartCommand = if config.modules.services.steamcmd.restart.command != null then config.modules.services.steamcmd.restart.command else "systemctl restart ${lib.escapeShellArg config.modules.services.steamcmd.serviceName}.service"; in { users.groups.${config.modules.services.steamcmd.group} = { }; users.users.${config.modules.services.steamcmd.user} = { isSystemUser = true; uid = config.registry."stable-uid".${config.networking.hostName}.steamcmd; group = config.modules.services.steamcmd.group; home = config.modules.services.steamcmd.steamDir; }; systemd.tmpfiles.rules = [ "d ${config.modules.services.steamcmd.steamDir} 0770 ${config.modules.services.steamcmd.user} ${config.modules.services.steamcmd.group} - -" "z ${config.modules.services.steamcmd.steamDir} 0770 ${config.modules.services.steamcmd.user} ${config.modules.services.steamcmd.group} - -" "d ${config.modules.services.steamcmd.installDir} 0770 ${config.modules.services.steamcmd.user} ${config.modules.services.steamcmd.group} - -" "z ${config.modules.services.steamcmd.installDir} 0770 ${config.modules.services.steamcmd.user} ${config.modules.services.steamcmd.group} - -" "d ${config.modules.services.steamcmd.stateDir} 0770 ${config.modules.services.steamcmd.user} ${config.modules.services.steamcmd.group} - -" "z ${config.modules.services.steamcmd.stateDir} 0770 ${config.modules.services.steamcmd.user} ${config.modules.services.steamcmd.group} - -" ]; systemd.services = { "steamcmd-${config.modules.services.steamcmd.serviceName}-update" = { description = "Update ${config.modules.services.steamcmd.serviceName} with SteamCMD"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; serviceConfig = { Type = "oneshot"; User = config.modules.services.steamcmd.user; Group = config.modules.services.steamcmd.group; WorkingDirectory = "-${config.modules.services.steamcmd.steamDir}"; ExecStartPre = "+${prepareScript}"; ExecStart = updateScript; }; }; ${config.modules.services.steamcmd.serviceName} = { description = "${config.modules.services.steamcmd.serviceName} game server"; after = [ "network-online.target" "steamcmd-${config.modules.services.steamcmd.serviceName}-update.service" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; path = [ pkgs.steam-run ]; serviceConfig = { Type = "simple"; User = config.modules.services.steamcmd.user; Group = config.modules.services.steamcmd.group; WorkingDirectory = "-${config.modules.services.steamcmd.installDir}"; EnvironmentFile = config.modules.services.steamcmd.environmentFiles; ExecStartPre = [ "+${prepareScript}" ] ++ lib.optional config.modules.services.steamcmd.updateOnStart updateScript; ExecStart = startScript; Restart = "on-failure"; }; }; "steamcmd-${config.modules.services.steamcmd.serviceName}-restart" = lib.mkIf config.modules.services.steamcmd.restart.enable { description = "Restart ${config.modules.services.steamcmd.serviceName}"; serviceConfig = { Type = "oneshot"; ExecStart = pkgs.writeShellScript "steamcmd-${config.modules.services.steamcmd.serviceName}-restart" '' set -eu exec ${restartCommand} ''; }; }; }; systemd.timers."steamcmd-${config.modules.services.steamcmd.serviceName}-restart" = lib.mkIf config.modules.services.steamcmd.restart.enable { description = "Restart ${config.modules.services.steamcmd.serviceName} on a schedule"; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = config.modules.services.steamcmd.restart.onCalendar; RandomizedDelaySec = config.modules.services.steamcmd.restart.randomizedDelaySec; Persistent = true; }; }; networking.firewall.allowedTCPPorts = config.modules.services.steamcmd.tcpPorts; networking.firewall.allowedUDPPorts = config.modules.services.steamcmd.udpPorts; } )) ]; }