# cs16-dm-server setup for Windows 10/11: double-click setup.cmd (which runs this). # setup.cmd first install (asks a few questions), or re-run safely any time # setup.cmd -Yes no questions: use settings.txt as it is (or the defaults) # setup.cmd -Update also update the game files from Steam (then re-installs the mods) # Steps: checks -> install folder -> questions -> secrets -> SteamCMD + CS 1.6 (~600 MB) # -> mod stack -> firewall -> start -> "how will your friends join?" -> summary. param([switch]$Yes, [switch]$Update) . (Join-Path $PSScriptRoot 'common.ps1') trap { Write-Host '' Write-Host "error: $($_.Exception.Message)" -ForegroundColor Red Write-Host 'Fix the problem above, then double-click setup.cmd again (it continues where it stopped).' exit 1 } function Ask($question, $default) { if ($Yes) { return $default } $r = Read-Host " $question [$default]" if ($r -eq '') { return $default } return $r.Trim() } function New-Secret { $chars = [char[]]'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' $bytes = New-Object byte[] 32 (New-Object Security.Cryptography.RNGCryptoServiceProvider).GetBytes($bytes) -join ($bytes | ForEach-Object { $chars[$_ % $chars.Length] }) } $repo = Split-Path -Parent $PSScriptRoot Say 'Checking this PC' if (-not [Environment]::Is64BitOperatingSystem) { throw 'this needs 64-bit Windows 10 or 11' } if ([Environment]::OSVersion.Version.Major -lt 10) { throw 'this needs Windows 10 or 11' } Write-Host ' ok' Say 'Install folder' $def = Resolve-InstallDir ([Environment]::GetFolderPath('MyDocuments')) $env:USERPROFILE @($env:OneDrive, $env:OneDriveConsumer, $env:OneDriveCommercial) # Remembered install folder, so running setup.cmd from the downloaded copy again updates that install $rememberFile = Join-Path $env:LOCALAPPDATA 'cs16-dm-server\install-folder.txt' $known = $null if (Test-Path -LiteralPath $rememberFile) { $k = [IO.File]::ReadAllText($rememberFile).Trim() if ($k -and (Test-Path -LiteralPath (Join-Path $k 'settings.txt'))) { $known = $k } } $repoFull = [IO.Path]::GetFullPath($repo).TrimEnd('\') if ((Test-Path -LiteralPath (Join-Path $repo 'settings.txt')) -or ($repo -ieq $def.Path)) { $Root = $repo # already installed here: update in place Write-Host " $Root" } else { if ($known) { $Root = $known Write-Host " updating your server in $Root" } else { if ($def.Reason) { Warn " $($def.Reason)" } while ($true) { $Root = (Ask 'Install the server in' $def.Path).Trim().Trim('"').TrimEnd('\') # "Copy as path" adds quotes $problem = Test-InstallDir $Root if (-not $problem) { $full = [IO.Path]::GetFullPath($Root) if ($full -ieq $repoFull -or $full.StartsWith($repoFull + '\', [StringComparison]::OrdinalIgnoreCase)) { $problem = 'pick a folder outside the downloaded files' } } if (-not $problem) { break } Warn " $problem" if ($Yes) { throw $problem } } } New-Item -ItemType Directory -Force -Path $Root | Out-Null $skip = 'hlds', 'dl', 'steamcmd', 'secrets', 'settings.txt', 'kills-tally.json', '.git' foreach ($item in Get-ChildItem -LiteralPath $repo -Force) { if ($skip -contains $item.Name) { continue } Copy-Item -LiteralPath $item.FullName -Destination $Root -Recurse -Force } # files from a browser download are marked "from the internet": Windows would warn on every run Get-ChildItem -LiteralPath $Root -Recurse -File | Unblock-File Write-Host " copied the server files to $Root" } $drive = Get-PSDrive -Name $Root.Substring(0, 1) -ErrorAction SilentlyContinue if ($drive -and $drive.Free -lt 2GB -and -not (Test-Path -LiteralPath (Join-Path $Root 'hlds\hlds.exe'))) { throw "not enough disk space on $($drive.Name): (needs 2 GB free)" } New-Item -ItemType Directory -Force -Path (Split-Path $rememberFile) | Out-Null Write-TextFile $rememberFile $Root $H = Join-Path $Root 'hlds' $settingsFile = Join-Path $Root 'settings.txt' Say 'Settings' if (-not (Test-Path -LiteralPath $settingsFile)) { $d = $Script:DefaultSettings $name = Ask 'Server name' $d.CS_HOSTNAME $pw = Ask 'Join password (Enter = open server, anyone can join)' '' $m = Ask 'Game mode: dm = deathmatch, gg = GunGame' $d.CS_MODE if ($m -ne 'gg') { $m = 'dm' } $b = Ask 'Bots (fill the server up to this many players, 0 = none)' $d.CS_BOTS if ($b -notmatch '^\d+$') { $b = '0' } $diff = Ask 'Bot difficulty (0 easy .. 4 expert)' $d.CS_BOT_DIFFICULTY if ($diff -notmatch '^[0-4]$') { $diff = $d.CS_BOT_DIFFICULTY } Write-TextFile $settingsFile "# cs16-dm-server settings. Edit, then restart the server (cs.cmd -> 7). No double quotes in values.`r`n" foreach ($k in $d.Keys) { Write-Setting $settingsFile $k $d[$k] } Write-Setting $settingsFile 'CS_HOSTNAME' $name Write-Setting $settingsFile 'CS_PASSWORD' $pw Write-Setting $settingsFile 'CS_MODE' $m Write-Setting $settingsFile 'CS_BOTS' $b Write-Setting $settingsFile 'CS_BOT_DIFFICULTY' $diff Write-Host ' saved in settings.txt' } else { Write-Host ' using settings.txt' } $sec = Join-Path $Root 'secrets' New-Item -ItemType Directory -Force -Path $sec | Out-Null if (-not (Test-Path -LiteralPath "$sec\rcon-password.txt")) { Write-TextFile "$sec\rcon-password.txt" (New-Secret) } # ReUnion salt: non-Steam players' IDs are derived from it. Keep it, or their stats change. if (-not (Test-Path -LiteralPath "$sec\reunion-salt.txt")) { Write-TextFile "$sec\reunion-salt.txt" (New-Secret) } # the running server holds the engine files open: stop it while they're replaced if (Stop-CsServer $Root) { Write-Host ' stopped the running server for the update' } $s = Read-Settings $settingsFile $port = [int]$s.CS_PORT if (-not (Test-UdpPortFree $port)) { $free = $port + 1 while ($free -lt $port + 50 -and -not (Test-UdpPortFree $free)) { $free++ } Warn " UDP port $port is in use by another program." $ans = Ask "Use port $free instead? (y/n)" 'y' if ($ans -notmatch '^y') { throw "port $port is busy: close the program using it, or set CS_PORT in settings.txt" } Write-Setting $settingsFile 'CS_PORT' $free $port = $free } Write-Host " server '$($s.CS_HOSTNAME)', UDP port $port, bots $($s.CS_BOTS)" if (-not (Test-Path -LiteralPath (Join-Path $H 'cstrike\liblist.gam')) -or $Update) { Say 'Downloading Counter-Strike 1.6 server files (SteamCMD, about 600 MB, takes a while)' $sc = Join-Path $Root 'steamcmd' if (-not (Test-Path -LiteralPath "$sc\steamcmd.exe")) { New-Item -ItemType Directory -Force -Path $sc | Out-Null Get-Url 'https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip' "$sc\steamcmd.zip" Expand-ZipPart "$sc\steamcmd.zip" '' $sc Remove-Item -LiteralPath "$sc\steamcmd.zip" } # validate only on the first download: it would also put back Valve's versions of edited files $first = -not (Test-Path -LiteralPath (Join-Path $H 'cstrike\liblist.gam')) $mc = Join-Path $H 'cstrike\mapcycle.txt'; $mcKeep = Join-Path $Root 'dl\mapcycle.txt.keep' if (Test-Path -LiteralPath $mc) { New-Item -ItemType Directory -Force -Path (Split-Path $mcKeep) | Out-Null; Copy-Item -LiteralPath $mc -Destination $mcKeep -Force } # steam_legacy: ReHLDS needs engine build <= 8684. app 90 often fails on the first try (and # SteamCMD updates itself on its first run), so retry. $scArgs = @('+force_install_dir', $H, '+login', 'anonymous', '+app_set_config', '90', 'mod', 'cstrike', '+app_update', '90', '-beta', 'steam_legacy') if ($first) { $scArgs += 'validate' } $scArgs += '+quit' $ok = $false for ($i = 1; $i -le 4; $i++) { $ErrorActionPreference = 'Continue' & "$sc\steamcmd.exe" @scArgs | Out-Host $ErrorActionPreference = 'Stop' if ((Test-Path -LiteralPath (Join-Path $H 'cstrike\liblist.gam')) -and (Test-Path -LiteralPath (Join-Path $H 'hlds.exe')) -and $LASTEXITCODE -eq 0) { $ok = $true; break } Write-Host " SteamCMD attempt $i didn't finish, trying again..." } if (-not $ok) { throw 'game download failed: check your internet connection and double-click setup.cmd again' } if (Test-Path -LiteralPath $mcKeep) { Move-Item -LiteralPath $mcKeep -Destination $mc -Force } } else { Write-Host ''; Write-Host ' game files present (setup.cmd -Update to update them)' } Say 'Installing the mod stack' & (Join-Path $Root 'windows\install.ps1') -Root $Root if (-not (Test-FirewallRule $Root)) { Say 'Windows Firewall' if ($Yes) { Write-Host ' skipped (-Yes): allow it later with cs.cmd -> 10' } else { Write-Host ' Players on other PCs can only join when Windows Firewall lets the server in.' Write-Host ' Windows will ask for admin permission once (say Yes).' if (Add-FirewallRule $Root) { Write-Host ' allowed' -ForegroundColor Green } else { Warn ' not changed: only this PC can join for now. Try again with cs.cmd -> 10.' } } } Say 'Starting the server' [void](Start-CsServer $Root) Write-Host -NoNewline ' waiting for it to answer' $info = Wait-CsServer $port 60 if (-not $info) { Write-Host '' $missing = @('hlds.exe', 'swds.dll', 'cstrike\dlls\mp.dll', 'cstrike\addons\metamod\metamod.dll', 'cstrike\addons\amxmodx\dlls\amxmodx_mm.dll', 'cstrike\addons\yapb\bin\yapb.dll') | Where-Object { -not (Test-Path -LiteralPath (Join-Path $H $_)) } if ($missing) { Warn " missing: $($missing -join ', ') - maybe removed by your antivirus. Allow the folder $Root in it, then run setup.cmd again." } Write-Host ' last lines from the server:' Get-ConsoleTail $Root 20 | ForEach-Object { Write-Host " $_" } throw "the server didn't answer within 60 s (full log: $H\qconsole.log)" } Write-Host ' up' Write-Host " $(Format-A2SInfo $info)" if (-not $Yes) { & (Join-Path $Root 'windows\join.ps1') -Root $Root } Say 'Done' $s = Read-Settings $settingsFile Write-Host ' Join from Counter-Strike 1.6: press ~ for the console, then type' Write-Host " connect 127.0.0.1:$port (on this PC)" -ForegroundColor Green foreach ($ip in Get-LanIPv4) { Write-Host " connect ${ip}:$port (same house)" -ForegroundColor Green } if ($s.CS_PUBLIC_ADDR) { Write-Host " connect $($s.CS_PUBLIC_ADDR) (friends over the internet)" -ForegroundColor Green } Write-Host '' Write-Host " The server runs in its own window (minimized in the taskbar). Close that window to stop it." Write-Host " Menu (start, stop, map, mode, bots, kill counter, ...): double-click cs.cmd in" Write-Host " $Root" if ($repo -ine $Root -and -not $Yes) { Start-Process explorer.exe -ArgumentList "`"$Root`"" }