Infinite Half-Life: Deathmatch Stream

2023-10-27

tl;dr: theoretically unbounded-in-length HLDM stream up here. also, you can join the server (connect rqsall.com).

This article is related to a project I basically hacked together in a day or so - as such, the article was written in a similarly sloppy manner. :)

For whatever reason, I got the urge to set up a Half-Life: Deathmatch server again. I love the game, but haven't gotten to play as much as I'd like as I have 200+ ping to the active servers most times. I knew that even if I set up a server, probably no one would join, but meh.

You set up a Half-Life: Deathmatch (or Counter-Strike 1.6, or Richochet, or whatever) server by downloading it with a command-line tool known as steamcmd which will download it straight from Steam. More info here and here.

Notably, there's a reverse-engineered version of the Half-Life Dedicated Server (HLDS) known as ReHLDS which has lots of fixes. However, it seemed to break signoncommands in the hltv.cfg mentioned later and things got more stuttery when I tried it.

Then, I looked into adding bots to the server. A lot of documentation on running GoldSrc (the engine behind Half-Life) game servers isn't super readily found through searches, but I stumbled on jk_botti which has actually seen some commits within the past few years - the latest release is from 2017!

If you don't know, most HL server plugins rely on another plugin called "Metamod", which adds a bunch of functionality like dynamic loading of plugins. Installing Metamod was as simple as grabbing the latest Linux release from its Sourceforge release page, and placing the shared library from the provided archive to where it needed to be.

For GoldSrc games, there's a file called liblist.gam at the root of the game which specifies the location of the gamedlls among other things - the path to hl.so is replaced with metamod's library. From my understanding, a gamedll is basically the core of the "mod" loaded ("mods" are the game in question, e.g. Half-Life, Counter-Strike, Richochet). In my case, this meant changing the line gamedll_linux "dlls/hl.so" to gamedll_linux "addons/metamod/dlls/metamod.so".

By the way, the Half-Life "mod" lives under a folder simply called valve in the Half-Life Dedicated Server (HLDS)'s root. Counter-Strike 1.6 is a more descriptive "cstrike". Most of the paths here are relative to the valve/ directory.

After Metamod was installed, jk_botti was pretty simple too - just extract the latest release into addons/jk_botti and edit the config. I uncommented the min_ and max_bots variables, setting min_bots to 0 and leaving max_bots as the default. This means that the server will have 6 bots if no real humans are connected, and one gets kicked each time another human joins down to 0 bots. They'll rejoin as humans leave.

These bots are a real time capsule - they talk smack to each other (or you) in chat every now and then. A nice feature of jk_botti compared to a lot of other bots is that many other bot plugins required a precompiled list of waypoints for bots to go to - jk_botti seems to instead sort of figure it out itself (by default), and the bots supposedly path better as it rudimentarily learns from player movement.

I didn't want to rely on RCON for remotely issuing commands to the server console because I arbitrarily don't trust it, but I wanted to be able to send commands to the server even if it was daemonized. I learned of systemd's ListenFIFO option for sockets, which I hadn't seen discussed before. Basically, you can define a socket for a service and have it act as a FIFO for stdin/stdout/stderr.

My (sloppy) service looks like this:

[Unit]
Description=HLDM server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=user
WorkingDirectory=/home/user/hlds
ExecStart=/home/user/hlds/start.sh
Restart=on-failure
Sockets=hlds.socket
StandardInput=socket
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

My socket looks like this:

[Socket]
ListenFIFO=%t/hlds.stdin
Service=hlds.service

This results in me being able to do things like echo "map crossfire" > /run/hlds.stdin to change the map while the server is running instead of having to use RCON.

Also, I set the frag limit to 30 frags with mp_fraglimit 30 in server.cfg. I've also set some other things such as sv_proxies 1 (more about that one later).

So, pretty cool, we have a working HLDM server. Now what? I got a friend to play for a few maps one night, but I didn't want it to end there.

A companion CD called "Half-Life: Further Data" was released which had some extra content, most importantly to me a few new multiplayer maps and player models. Pretty much all you need to do is add the maps and models into the appropriate places on the server (just maps/ and models/player/), then change mapcycle.txt to include the new maps.

A neat feature of GoldSrc and Source servers is something called "FastDL". Basically, downloading the extra content directly from the server is really slow, so you can set sv_downloadurl in server.cfg to a URL of a website which has the content that may need to be downloaded accessible over HTTP. with sv_allowdownload 1, clients will try grabbing the extra content from the specified site rather than directly from the server. I ended up setting that up for this server.

But... now what? I realized that the bots are active even if no real players are connected, which gave me an idea: I could setup HLTV to watch them! If you don't know (though you might have guessed), HLTV is Half-Life Television. Basically, something called a "HLTV Proxy" connects to your server and then people can connect to it to spectate the ongoing game. I thought it could be fun to watch the bots from their perspective, so I decided to set it up. HLTV already comes with HLDS, so it's relatively simple to get going. Here's my hltv.cfg in all its glory (most of this is defaults from the hltv.cfg that used to ship with CS 1.6 at least apparently; notably, this goes in the HLDS root and not under valve/):

// HLTV Proxy configuration file

// HLTV proxy runs this file on start up
// This file should only be edited if you want to broadcast a game

// set HLTV proxy name as shown in score board
name "24/7 streamed @ twitch.tv/hlteevee default + further data maps"

// set HLTV name, how it should appear in game server browsers
hostname         "24/7 streamed @ twitch.tv/hlteevee default + further data maps"

// set offline info text clients will see as reject reason if HLTV isn't broadcasting yet
offlinetext "Sorry, game is delayed. Please try again later."
// delays broadcasting for 30 seconds
delay 10.0
// allow 3.5 KByte/sec as client rate. This is good a value
// for internet broadcasts. On LAN you may set this value to 10000
maxrate 10000
// log HLTV console in proxy.log
// logfile 1
// local chatting for HLTV spectators enabled
chatmode 1
// if game server is password protected, enable this line
// serverpassword    "mypassword"
// proxy's adim password for rcon, commentator etc.
// adminpassword    "mypassword"

// show message for 5 seconds each 60 seconds in center of X axis (-1) and
// above help text bar (0.85). Color given as hexadecimal RGBA .
// loopcmd 1 60 localmsg "You're watching HLTV. Visit www.valvesoftware.com" 5 -1 0.85 FFA000FF
loopcmd 1 60 localmsg "You're watching HLTV. Visit twitch.tv/hlteevee" 5 -1 0.85 FFA000FF

// hltv.tga will be shown instead of the default HLTV logo in spectator GUI
// bannerfile "hltv.tga"

// these commands will be executed on connecting spectator client and may be used
// to adjust settings for HLTV (for example voice parameters)
// signoncommands "voice_scale 2; voice_overdrive 16; volume 0.5; echo Voice adjusted for HLTV"

// first-person
signoncommands "spec_mode 4; spec_autodirector 1"


echo hltv.cfg loaded.

Of course, I changed the name and hostname (which shows in the server browser). I changed maxrate 3500 to maxrate 10000 to allow 10 KB/s of bandwidth between clients and the server instead of 3.5 KB/s, but if anyone is having trouble connecting with their modem let me know and I can change this. I also changed the message that shows every minute to advertise a certain Twitch stream instead of Valve's site. The most important change was changing the signoncommands - the defaults are annoying, they change your volume and voice chat volume without asking! Plus, I had something actually useful to put there - spec_mode 4 causes clients to use first-person view instead of the default chase camera, which is what I wanted. Searching for how to do this at first didn't give me much great info, with someone on Reddit asking why someone would want to do this and not answering, but luckily this article from Valve is still up. Almost forgot: I changed the delay/buffering time from 30 seconds to 10 seconds because the delay during map changes was substantial with a 30 second delay.

I set up a similar systemd unit and socket for HLTV as well.

Then, I thought: hey, this is just going forever. I could stream the game 24/7! I guess it's sort of like that infinite Seinfeld AI stream or something, but we're having fun watching some (much) more primitive AI. :)

I at first tried running Half-Life in a VM on my ESX host, but performance was awful - less than 10 frames a second without even streaming. I thought it'd be bad (the Linux Steam version doesn't have the software renderer anymore, so we're relying on llvmpipe on pretty old Xeons here), I was just hoping it somehow wouldn't be that bad.

I ended up simply installing Debian on an unused OptiPlex 990, setting up Steam and Half-Life, as well as OBS to stream to Twitch. I encountered a really weird bug - on this machine, even with widescreen selected, the video option for 1280x720 wouldn't show up. For some reason, only a couple 5:4 resolutions and 1600x900 showed up. I couldn't figure this out, and launch options like -width and -height didn't help, so I ended up just installing xdotool and resizing the window once the game was running:

xdotool search --name "Half-Life" windowsize 1280 720

Now, the stream is running! Check it out: twitch.tv/hlteevee!

If you join and the bots are all kinda just inanimate, or not much is happening besides the scoreboard showing, the round probably just ended and the server is in the process of switching the map.

The bots are actually pretty entertaining - they're set on the default skill (seems to be a ~medium difficulty) and they seem to have trouble with some of the maps in the pool (at least for now). :)

I don't have any fancy way of automatically launching the stream if the machine running it goes down, so don't expect this to be the most reliable thing ever. Also, just like the Seinfeld thing, this probably won't end up being anywhere close to infinite. Who knows, maybe it'll go down tonight while I'm asleep!

Oh, also, feel free to join the server!

connect rqsall.com

Image: Live!

EDIT 2023-10-28: The bots are really bad at some of the maps in the pool. If you want to help, join the server and play with them for a bit! Also, you can join HLTV too: connect rqsall.com:27020

EDIT 2023-10-28: I've increased max_bots to 11, which will result in 10 bots unless humans start connecting (turns out the HLTV Proxy counts as a player). This should help ensure there's more action on maps where the bots are currently a bit confused, plus matches on maps like crossfire see pretty much constant action now.

EDIT 2023-10-28: I've set it so all bots are skill 2 now, the second best. Before, the default config was having 5 bots from skills 1 through 5, then the rest being 3. Hopefully this should help a bit too.

EDIT 2023-11-11: I've now switched to streaming from a Windows VM on my ESXi server so the setup would be a bit more sustainable for me. However, now the game runs in software rendered mode at 640x480 4:3 instead of 1280x760 16:9. It's also locked to 20 FPS and I have to have cl_showfps enabled or some of the text rendering is bugged out for some reason. Some may view this as an upgrade. Also, since ESXi doesn't emulate a sound card, I'm using VB-CABLE to function as dummy input/output devices.