When I was growing up, I loved playing the classic 1993 game DOOM. This is why recently, as part of a talk I gave at APIs and IPAs, I decided to do a demo of how I embedded a RESTful API into DOOM, allowing the game to be queried and controlled using HTTP and JSON.
I wrote the entire API specification for the project in RAML––a RESTful API Modeling Language based on YAML that allows you to create standardized, reusable APIs. But before I get into the technical details of this project, let us step back in time for a minute.
1993
DOOM was created by a small team at ID Software. Wikipedia describes it as one of the most significant and influential titles in video game history.
ID Software has a great practice of releasing source code for their games. For the kind of hackers who lurk on /r/gamedev, an ID Software engine is an amazing resource to learn from. And lo, in 1997, the DOOM engine source code was released––causing much happiness!
2017
I was having trouble finding a fun API to use for the talk I had to do at APIs and IPAs (recording below). I had promised to give a talk about a cool use of APIs, I spent the normal amount of time procrastinating and stressing about presenting the talk, and I wasn’t making any progress on building a compelling demo.
Late one night, out of the blue, I had the idea to create an API for DOOM, now 24 years old and obviously never designed to have an API! With this project, I could have some fun digging around the DOOM source code and solve my API problem at the same time!
My random idea materialized into RESTful-DOOM –– a version of DOOM which really does host a RESTful API! The API allows you to query and manipulate various game objects with standard HTTP requests as the game runs.
There were a few challenges ahead of me:
- Build an HTTP+JSON RESTful API server in C.
- Run the server code inside the DOOM engine, without breaking the game loop.
- Figure out what kinds of things we can manipulate in the game world, and how to interact with them in memory to achieve the desired effect!
I picked chocolate-doom as the base DOOM code to build on top of. I liked this project because it aims to stick as close to the original experience as possible, while making it easy to compile and run on modern systems.
Hosting an HTTP API server inside DOOM
The project chocolate-doom already uses SDL, so I added an -apiport <port> command line arg and used SDLNet_TCP_Open to open a TCP listen socket on startup. Servicing client connections while the game is running is a bit trickier, because the game must continue to update and render the world many times a second, without delay. We must not make any blocking network calls.
The first change I made was to edit D_ProcessEvents (the DOOM main loop), to add a call to our new API servicing method API_RunIO. This calls SDLNet_TCP_Accept, which accepts a new client, or immediately returns NULL if there are no clients.
If we have a new client, we add its socket to a SocketSet by calling SDLNet_TCP_AddSocket. Being part of a SocketSet allows us to use the non-blocking SDLNet_CheckSockets every tic to determine if there is data available.
If we do have data, API_ParseRequest attempts to parse the data as an HTTP request, using basic C string functions. I used cJSON and yuarel libraries to parse JSON and URI strings respectively.
Routing an HTTP request involves looking at the method and path, then calling the right implementation for the requested action. Below is a snippet from the API_RouteRequest method:
if (strcmp(path, "api/player") == 0) { if (strcmp(method, "PATCH") == 0) { return API_PatchPlayer(json_body); } else if (strcmp(method, "GET") == 0) { return API_GetPlayer(); } else if (strcmp(method, "DELETE") == 0) { return API_DeletePlayer(); } return API_CreateErrorResponse(405, "Method not allowed"); }
Each action implementation (for example API_PatchPlayer) returns an api_response_t containing a status code and JSON response body.
Putting it all together, this is what the call graph looks like when handling a request for PATCH /api/player:
D_ProcessEvents(); API_RunIO(); SDLNet_CheckSockets(); SDLNet_TCP_Recv(); API_ParseRequest(); API_RouteRequest(); API_PatchPlayer(); API_SendResponse();
Interfacing with DOOM entities
Building an API into a game not designed for it is actually quite easy when the game is written in straight C. There are no private fields or class hierarchies to deal with. And the extern keyword makes it easy to reference global DOOM variables in our API handling code, even if it feels a bit dirty.
We want the API to provide access to the current map, map objects (scenery, powerups, monsters), doors, and the player. To do these things, we must understand how the DOOM engine handles them.
The current episode and map are stored as global int variables. By updating these values, then calling the existing G_DeferedInitNew, we can trigger DOOM to switch smoothly to any map and episode we like.
Map objects (mobj_t) implement both scenery items and monsters. I added an id field which gets initialized to a unique value for each new object. This is the id used in the API for routes such as /api/world/objects/:id.
To create a new map object, we call the existing P_SpawnMobj with a position and type. This returns us an mobj_t* that we can update with other properties from the API request.
The local player (player_t) is stored in the first index of a global array of players. By updating fields of the player, we can control things like health and weapon used. Behind the scenes, a player is also an mobj_t.
A door in DOOM is a line_t with a special door flag. To find all doors, we iterate through all line_t in the map, returning all lines which are marked as a door. To open or close the door, we call the existing EV_VerticalDoor to toggle the door state.
API Specification
Earlier in the blog post, I mentioned the term ‘API specification.’ API specification describes the HTTP methods, routes, and data types that the API supports. For example, it will tell you the type of data to send in a POST call to /api/world/objects, and the type of data you should expect in response.
I wrote the API spec in RAML 1.0. It is also hosted in a public API Portal for easier reading.
Putting it all together
So now we have an HTTP+JSON server inside DOOM, interfacing with DOOM objects in memory, and have written a public API specification for it. Phew!
We can now query and manipulate this 24-year-old game from any REST API client and here is a video proving exactly that!
If you enjoyed this project, then check out the Github repo and demo, and enjoy!
This article first appeared on 1amstudios