Bonjour with the Source Engine
April 16th, 2012
A while ago I had the idea of using a second monitor or computer to display game information whilst gaming, sorta like the G15 keyboard on steroids. It’s easier to glance over to than bringing up another HUD panel and let’s you split focus on the game and the Starks since one doesn’t directly block your view of the other. Whilst I’m not sure if it would have any lasting value, it’s similar to the TF2 kill counter Arduino project. Since I got an iPad, though, I thought I’d finally give it a shot on that with the Source Engine. There are a few components needed to make it all come together though:
Firstly, I need a way to get game data out of that source engine. Fortunately, since almost 18 months ago the Source engine supports client plugins, unfortunately the only documentation on it is a wiki page full of red links. I also need an SDK for the said plugin, however the latest version of the Source SDK is Source 2007, which none of Valve’s games use any more. They’re all on Source 2009 for which there is no public SDK, or the Left 4 Dead engine series which also has no public SDK, save for Alien Swarm. AlliedModders have done a good job at reverse-engineering the interfaces provided by these engines, so I can still build a plugin for them.
The next thing I need is a client application to connect to the Source instance, which I’ll worry about once I get the basics working. I’m not going to put hours into an interface when the underlying core might not even work out.
Thirdly, I need a way for the two to communicate. The logical choice for me is to use TCP as I don’t want to miss a game event, and after some experience with Google Protocol Buffers from playing around with SteamKit I’m tempted to use them again here. It takes the burden of writing serialization and deserialization code and lets me just focus on the data itself.
Lastly, I need a way for the client and the Source instance to discover each other. I opted to use Bonjour for this, because it’s relatively painless from a user perspective. It doesn’t get much simpler than just selecting your device from an automagic list. It’s also available for both Windows, Mac (for which there is no Source SDK yet) and iOS.
Working out from the game data to something I’d see on the iPad, the first step was the client plugin.
With a total lack of documentation for client plugins, starting one was hard. According to the one snippet of documentation that does exist, however, they’re just an extension of server plugins, so I decided to start there.
The Source Engine exposes two main interfaces for plugins to subclass, IServerPluginCallbacks and IGameEventListener2. The SDK also provides a handy macro to expose it as a global singleton, so that bit was easy.
I decided to start with Left 4 Dead 2 but ran into a couple of hiccups out of the gate, though - the first being that with very few modifications to the serverplugin_empty example that Valve provides, my plugin failed to load. It took a bit of debugging before I figure out that the filesystem interface (IFileSystem/g_pFullFileSystem) wasnt initializing. Instead of fixing it, I removed it. I don’t need filesystem access anyway (at least yet).
I also couldn’t quite figure out how to build against the new logging subsytem introduced in Left 4 Dead 2, so I went back to using the legacy Msg() and Error() functions.
After discovering the joys (read: pains) of user IDs, entity IDs and edicts, I got to a point where the client plugin will display a message when a player or zombie dies, printing who killed who with what weapon. Thanks to everyone in IRC who helped me with that and more.
With the basic interfacing working, it was time to add Bonjour into the mix. Linking against Bonjour was fine, but I also needed to link against ws2_32.lib just to use htonl(). Why can’t stuff just be included by default? After that, though, is where some ‘fun’ happened:
After wondering why my DNSServerRegisterReply callback wasn’t being called, I discovered that Bonjour seems to require you to write your own runloop for it. With most of my Bonjour code gutted from the dns-sd source included with the SDK, I grabbed the relevant bits and put them (temporarily) into the GameFrame() plugin method which runs every frame. That works, but only while the game is actually running. It doesn’t work at the main menu. I’m not sure at what point client plugins are loaded normally (without running the plugin_load console command), so this may not actually matter if it’s loaded in-game anyway.
And this is where I got stuck for today. I’m passing in the exact same parameters to DNSServiceRegister() that dns-sd does. I have the exact same (perhaps not as frequently, though) polling code that dns-sd does. Yet somehow, the plugin registers itself on the network (and can detect name collisions) but doesn’t respond to any queries, and registration with dns-sd still works fine.