Preface

My friends and I play a lot of games online. Destiny 2, Diablo 3, LoL, Dying Light, etc. The list goes on. We have played Garry’s Mod (GMod) in the past and it has been a ton of fun. However, the setup was a pain and it ended up being on one persons computer so we had to rely on them to be A) online B) wanting to play the game C) not broken something D) their internet connection being good enough to support everyone playing. Usually, one of those would break down and we would give up because we didn’t want to wait, some of us have limited time to game.

Solution, hint it’s always code

I started this challenge with a simple base. We all use windows. Added this, I know powershell scripting relatively well. So I started down the path following this guide from ajgeiss0702 on Steam. This is a great starting point for how this script will lay out.

So I started in with powershell. I knew that one thing I wanted to allow for variance was install location of the server so my “base” powershell script used a few params. I will show all of them now and refer back to them throughout this.

param(
  [Parameter()]
  [string]$game,
  [Parameter()]
  [string]$serverName,
  [Parameter()]
  [string]$map,
  [Parameter()]
  [string]$installLocation,
  [Parameter()]
  [boolean] $forceUpdate
)

I separated out some of the “helper” functions into a separate file so my first step was importing them all. . ".\DedicatedServer\DSHelpers.ps1". Next I wanted to have some defaulting and safeguarding around the other params.

 if (!$installLocation) {
  $installLocation = "C:\sourceServer";
}
if (!$serverName) {
  $serverName = "Tucker_Smells";
}
switch($game) {
  "$_ -eq murder" {
    $gameMode = "murder";
    break;
  }
  "$_ -eq ttt" { continue; }
  "$_ -eq tt" { continue; }
  "$_ -eq trouble in terrorist town" {
    $gameMode = "terrortown";
    break;
  }
  "$_ -eq prop_hunt" { continue; }
  "$_ -eq obj_hunt" { continue; }
  "$_ -eq prophunt" { continue; }
  "$_ -eq objhunt" {
    $gameMode = "prop_hunt";
    break;
  }
  default { 
    $gameMode = "prop_hunt";
    break;
  }
}
$gamenameArg = "+gamemode $gameMode"

$serverName = $serverName.trim()
$serverName = $serverName.replace(' ', '_')
$installLocation = $installLocation.TrimEnd("\");

After that I configured the locations of executables and the directories for the applications.

$steamExe = "$installLocation\steamcmd.exe"
$gmodDir = "$installLocation\steamapps\common\GarrysModDS"
$gmodExe = "$gmodDir\srcds.exe"

Next is where some of the magic starts, we are going to call all 4 of the helper functions

CheckOrInstallSteamCmd -installLocation $installLocation;
CloneObjHuntRepo -installLocation $gmodDir -forceUpdate $forceUpdate;
SetupServerConfig -gmodDir $gmodDir;
$mapsList = SetupMapRotation -gmodDir $gmodDir -gameMode $gameMode;

We will talk about these briefly and then look at the code, one at a time.

Install SteamCMD

This says and does basically what you’d expect. It checks to see if steamcmd.exe exists at the $installLocation (defaulting to C:\sourceServer). If that doesn’t exist we are going to grab the installer zip file from steam, unzip it to our $installLocation and remove the zip (be a good citizen, don’t leave your trash).

Function CheckOrInstallSteamCmd {
  param(
    [Parameter()]
    [string] $installLocation
  )
  
  Add-Type -AssemblyName System.IO.Compression.FileSystem
  if (!(Test-Path $installLocation\steamcmd.exe)) {
    Invoke-WebRequest "http://media.steampowered.com/installer/steamcmd.zip" -outfile $env:USERPROFILE\Downloads\steamcmd.zip
    [System.IO.Compression.ZipFile]::ExtractToDirectory("$env:USERPROFILE\Downloads\steamcmd.zip", $installLocation);
    Remove-Item -Path $env:USERPROFILE\Downloads\steamcmd.zip -Recurse -Force
  }
}

CloneObjHunt

We prefer ObjHunt over the base prop_hunt for various reasons. They play very similarly but there are some minor differences that we don’t need to get into here. We like it so I did this. This functions a lot like the CheckOrInstallSteamCmd function we use the prop_hunt.txt file to see if we have the ObjHunt override instead of the base game. Before we can check the prop_hunt.txt file we want to see if it exists, if it doesn’t we create a dummy file. Then we check the first line, in the base prop_hunt it will read "prop_hunt" in Obj it will read "objhunt". If we don’t have ObjHunt or the $forceUpdate flag is set to true (this is based on the repo not the official version so there are more likely to be updates), we will git clone the whole repo into the user’s download directory, remove any residual leftovers from the prop_hunt folder, move all the items in the objhunt repo folder to the prop_hunt folder, and then clean the download (again, be a good citizen!).

Function CloneObjHuntRepo {
  param(
    [Parameter()]
    [string] $installLocation,
    [Parameter()]
    [boolean] $forceUpdate
  )

  if (!(Test-Path $installLocation\garrysmod\gamemodes\prop_hunt\prop_hunt.txt)) {
    New-Item -Path $installLocation\garrysmod\gamemodes\prop_hunt -Name "prop_hunt.txt" -ItemType "file" -Value "empty"
  }
  $firstLineOfPropHunt = Get-Content $installLocation\garrysmod\gamemodes\prop_hunt\prop_hunt.txt -First 1
  if ($firstLineOfPropHunt -notmatch "objhunt" -or $forceUpdate) {
    git clone https://github.com/Newbrict/ObjHunt.git $env:USERPROFILE\Downloads\objhunt
    Remove-Item -Path $installLocation\garrysmod\gamemodes\prop_hunt -Recurse -Force
    Move-Item -Force $env:USERPROFILE\Downloads\objhunt\* $installLocation\garrysmod\gamemodes\prop_hunt
    Remove-Item -Path $env:USERPROFILE\Downloads\objhunt -Recurse -Force
  }
}

SetupServerConfig

This one is very straight forward and, frankly, kind of boring. We set a variable with a bunch of values in it that are how our server should run. After that we output the content of the variable to the cfg/server.cfg file. Meh.

Function SetupServerConfig {
  param(
    [Parameter()]
    [string]$gmodDir
  )
  $serverConfig = '// Hostname for server.
// RCON - remote console password.
rcon_password "xxxxx!"

// Server password - for private servers.
sv_password "xxxxx"

// Server Logging
log on
sv_logbans 1
sv_logecho 1
sv_logfile 1
sv_log_onefile 0
lua_log_sv 0

sv_rcon_banpenalty 0
sv_rcon_maxfailures 20
sv_rcon_minfailures 20
sv_rcon_minfailuretime 20

// Network Settings
sv_downloadurl ""
sv_loadingurl ""
net_maxfilesize 64
sv_maxrate 0
sv_minrate 800000
sv_maxupdaterate 66
sv_minupdaterate 33
sv_maxcmdrate 66
sv_mincmdrate 33

// Server Settings
sv_airaccelerate 100
sv_gravity 600
sv_allow_voice_from_file 0
sv_turbophysics 0
sv_client_min_interp_ratio 1
sv_client_max_interp_ratio 2
think_limit 20
sv_region 0
sv_noclipspeed 5
sv_noclipaccelerate 5
sv_lan 0
sv_alltalk 1
sv_contact server_admin@matterickson.me
sv_cheats 0
sv_allowcslua 0
sv_pausable 0
sv_filterban 1
sv_forcepreload 1
sv_footsteps 1
sv_voiceenable 1
sv_timeout 120
sv_deltaprint 0
sv_allowupload 0
sv_allowdownload 0

// Sandbox Settings
sbox_noclip 0
sbox_godmode 0
sbox_weapons 0
sbox_playershurtplayers 0
sbox_maxprops 100
sbox_maxragdolls 50
sbox_maxnpcs 10
sbox_maxballoons 10
sbox_maxeffects 0
sbox_maxdynamite 0
sbox_maxlamps 5
sbox_maxthrusters 20
sbox_maxwheels 20
sbox_maxhoverballs 20
sbox_maxvehicles 1
sbox_maxbuttons 20
sbox_maxemitters 0

// Misc Config$
exec banned_user.cfg
exec banned_ip.cfg
heartbeat';
  $serverConfig | Set-Content $gmodDir\garrysmod\cfg\server.cfg;
}

Setup the map rotation

This one is about as boring. We have a list of maps that are playable and, depending on the gameMode selected, we populate a list and output it to the cfg/mapcycle.txt file. We do return the list of maps to the main function for later use though.

Function SetupMapRotation {
  param(
    [Parameter()]
    [string]$gmodDir,
    [Parameter()]
    [string]$gameMode
  )
  #between fountain and arena  is supposed to be "Awesome Building - Prop Hunt"
  if ($gameMode -eq 'prop_hunt') {
    $maps = "ph_restaurant
ph_nightofthelivingdead
ph_office
ph_fancyhouse
ph_parkinglot
ph_starship
ph_funeral_home
ph_fountain
ph_arena
ph_chinese
ph_bank
ph_saltfactory
ph_cliffyard
ph_motelblacke_redux
ph_apartment_v2
ph_spieje
ph_house_v3
ph_mansion_v1
ph_diordna_hotel_2
ph_houseplace
ph_gas_stationrc7_xmas
ph_grand_hotel_night";
  } elseif ($gameMode -eq 'terrortown') {
    $maps = "ttt_nuclear_power_b2
ttt_camel_v1
ttt_old_factory
ttt_sewer_system
ttt_skycity4
ttt_slender_v3_fix
ttt_stargate
ttt_theship_v1
ttt_aircraft_v1b
ttt_subway_b4
ttt_minecraft_b5
ttt_swimming_pool_v2
ttt_traitorville_v2
ttt_bb_teenroom_b2
ttt_bb_outpost57_b5
ttt_fastfood_a6
ttt_community_bowling
ttt_chaser
ttt_icarus_r1_a2
ttt_lttp_kakariko
ttt_lumos
ttt_mc_skyislands
ttt_metropolis
ttt_richland
ttt_vessel
ttt_terrortown
ttt_casino_b2
ttt_cluedo_b5_improved1
ttt_oldruins";
  }
  $maps | Set-Content $gmodDir\garrysmod\cfg\mapcycle.txt;
  return $maps;
}

Back home in the main function

We start up the steamcmd.exe to update garry’s mod. app_update 4020 is garry’s mod. We pass in the -NoNewWindow and -Wait so we don’t get more windows and execution stops while this completes.

$args = "+login anonymous +app_update 4020 validate +quit"
Start-Process -NoNewWindow -Wait -FilePath $steamExe -ArgumentList $args

Here is kind of a fun part, if the user doesn’t pass in a -map param we get to grab a random starting map.

if (!$map) {
  $map = $mapsList -Split '\r?\n' | Get-Random
}

Start up the actual server

This last part we use the collection I have put together for trouble in terrorist town and objhunt and kick off the server

$workshopGameArg = "+host_workshop_collection 2040200286"
$args = "-console +hostname $serverName -authkey <insert_your_key> $workshopGameArg -game garrysmod $gamenameArg +exec server.cfg +map $map"
Start-Process -NoNewWindow -FilePath $gmodExe -ArgumentList $args

Lastly, because this is intended for people who may not know all of the ins and outs of how to run a dedicated server I put together this gross graphic to give some helpful tips.

Write-Host -ForegroundColor Red    "--------------------------------srcds.exe commands (the other window)-----------------------------------------------"
Write-Host -ForegroundColor Yellow "| gamemode *mode*                              | changes game mode                                                 |"
Write-Host -ForegroundColor Yellow "| changelevel *map_name*                       | changes the map                                                   |"
Write-Host -ForegroundColor Yellow "| status                                       | gives information about the running server (including ip address) |"
Write-Host -ForegroundColor Yellow "| quit                                         | exits (shut down the server)                                      |"
Write-Host -ForegroundColor Red    "--------------------------------in game dev console commands (~)----------------------------------------------------"
Write-Host -ForegroundColor Green  "| rcon_password tH3_pw                         | drop into 'admin' mode                                            |"
Write-Host -ForegroundColor Green  "| rcon changelevel *map_name* (ph, tt, etc)    | changes the level of the server                                   |"
Write-Host -ForegroundColor Green  "| rcon gamemode *mode* (terrortown, prop_hunt) | changes the game mode                                             |"
Write-Host -ForegroundColor Red    "--------------------------------------------------------------------------------------------------------------------"
Write-Host -ForegroundColor Green  "| you can test these commands as 'changelevel' to find the map and then use 'rcon' to change ther server map       |"
Write-Host -ForegroundColor Green  "| also helpful link for commands https://steamcommunity.com/sharedfiles/filedetails/?id=170589737                  |"
Write-Host -ForegroundColor Red    "--------------------------------------------------------------------------------------------------------------------"
$csvMapList = $mapsList -Split '\r?\n' -Join ', '
Write-Host -ForegroundColor Green "The map list for $game: " -NoNewline
Write-Host -ForegroundColor Magenta $csvMapList