Windows version: host the server from your own PC
setup.cmd installs the same DM/GunGame server natively on Windows 10/11 (no Docker or Python) into Documents\cs16, then helps set up how friends join (LAN, playit.gg, port forwarding, VPN). cs.cmd is the menu. Also: setup falls back to backup copies of the freely licensed mod downloads on this Gitea when a main site is down (tools/make-mirror.sh fills them), and the README is rewritten as a plain guide. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
7e9cbee14d
commit
4b7fdcaa23
16 files changed
+1412
-72
No files matched your search
@@ -0,0 +1,205 @@
|
||||
# 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. 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`"" }
|
||||
Reference in new issue
Block a user