I made a thing; let me tell you about it
I play video games, and I like it when my friends play with me. So to enable that, I host a dedicated server for us. The game I'm particularly interested in is Ark: Survival Evolved. I'm not actually here to talk about games, so I'll just say that if you enjoy the open world survival genre, Ark is worth checking out.
This project wasn't directly about that, but rather Ark served as a catalyst for it. Because it's a persistent open world game, the simulation keeps running even when no one is connected. That means that all the decay timers and spawns and so on keep running when there's no one online to maintain them. So, that's the problem: the game plays itself without me, and I don't like that. The solution is to stop it when it's empty and let the players start it on demand. If these sound a bit like user stories to you, that's intentional. So how about I just write them out.
- As a player, I want to not lose progress while I am offline.
- As a player, I want the server to be available whenever I want to play.
- As the admin, I don't want to have to manually start and stop the server for my players
I'll spare you from having to read acceptance criteria (and me from writing them 😉); this is enough to frame the rest of the discussion. And if you'd like to follow along, it's all on github.
First, the Ark server itself. It's hosted on an Ubuntu VPS, and is configured as an Upstart service. That service handles start/stop and will perform patching as a pre-start action. It also provides an rcon interface, which makes it easy to inspect some amount of server state from the outside. Notably for this topic, what I get out of that is a count of connected players. So with a little bit of scripting, I can have a cron job periodically run which polls the server population and shuts it down if it's empty. That takes 8 lines of Bash, plus one crontab entry. And that solves the problem of having the game keep playing when there's no one around to enjoy it.
That also raises another problem: The server is down a lot, and the only way to start it is for an admin (aka me) to ssh in and do it. That's not satisfying for me or the other players. That's where the dashboard comes in. It has 2 general components:
- A web UI which gives some status info and controls to start the server.
- A Node.js webserver (Express) to (optionally) serve the UI, and provide an API.
The UI is incredibly primitive, just plain html, js, and css; and very little of those. With no compression and almost no minification, the total download size about 1.1kb. Since it's just for the use of a handful of friends, I didn't bother with more than a very little bit of styling. And because it's a one-off project with hardly any interactivity, I didn't bother with any elaborate client side logic or frameworks.
The API is simple, but there's a lot more to it than the client. It provides 3 end points:
/restart. Status is actually the most complicated. It gets information from the Ark server in two very different ways. One is from the Upstart service via shell commands. To simplify slightly, this will let us know that the game server is either stopped, stopping, starting, or running. If the server is running, then we can get more detailed information. To do that, the API will query the game server directly via the Steam server protocol. This gives the kind of information you would expect to see in a server browser, like map name, version, connected players, etc. All of this gets sent back to the client, which displays the running status and if available, the name/version string and total other players.
The other two endpoints are simpler, but require authentication so that only people I've allowed can start the server. This authentication is done with a pre-shared password. There are no user names or accounts, because that's not information that valuable for this project. The password itself is protected in flight by a digest-like hashing scheme. More specifically, the client is given a 10 minute session key, combines that with the password, hashes it, and POSTs the hash for authentication. On the API end, an Express middleware component manages the session key and compares the client's hash code to the expected value for the session. Assuming that succeeds, then the Start endpoint will just shell out and start the service. If it's already running, there's no harm in trying to start it again. Restart will check via steam server protocol if any players are connected, and refuse if so. If not, then it shells out and restarts the service. Restart is useful to speed up the patching process in the event a game patch is released mid-day (which was incredibly common for a while).
At this point, everything works, and all that's needed is to turn it on. The node server needs to be running as a user with sudo permissions for at least (and preferably at most) these commands
service ark status,
service ark start, and
service ark restart. I have configured the node server itself as another Upstart service, which simplifies issues like process ownership and automatically restarting in the event of a crash or system reboot.
If this was the only thing on the domain, I would just need to run it on port 80 and call it a day. But it's not the only thing. And, beyond that, I like my URLs to have paths rather than subdomains. Accounting for that is very simple to do. For this project, in the client I prepend a base path to all of the urls in use. Express makes this easy because the default configuration is to use an HTML templating engine (specifically Jade) to generate the client content. And so prefixing urls with some path is as simple as adding a path variable to them in the templates. Once that's done, you can configure your webserver of choice to proxy your path of choice to the dashboard server.
Of course I'm not done yet. I think it's silly to be dynamically generating static content all the time. I know that Express caches the generated content, but it still bothers me. Also, Express is great for development, but it's only ok as a webserver. I'd rather have Nginx serve all the static content (because it's super good at that), and just let Express handle the REST endpoints. This requires two things:
- We have to be able to create static content for Nginx to serve.
- We have to configure Nginx to serve content from the base url, but proxy the endpoints.
Thanks to Jade, number 1 is actually quite easy. Jade provides a command line interface which does the same thing on stdio as it would internally for Express. I love this, and even as common as it is, I wish it was genuinely universal (I'm looking at you, Handlebars 😠). So I created a small script which will run the templates and their config data through Jade and spit it out to the file system. Configuring Nginx was more involved, and if you're interested you can see what that looks like here. The end result is that I save about 30ms of page load time, and when I get around to doing it, I'll only have to update Nginx's configuration to serve over HTTPS (which would eliminate the need for the complicated password hash scheme).
And that's the thing I made.