nanoMMO

Data-driven MMO content in C++ with protobuf

Monsters and NPCs join the items as data both sides read, go out by kind, and a content hash keeps clients and worlds in step.

18 October 2026 · Watch on YouTube

Episode 27 of a series about building an MMO from scratch, with the focus on the engineering around the game. Last time our sword became data, in a typed file that the server and the client both read. This time the rest of the game's content follows: the items folder becomes a content folder, with the kinds of monsters and NPCs and where they stand, in protobuf's text format, with checks for what types can't catch. Monsters and NPCs then go to the players by their kind's id, in place of their names and looks, which halves what the NPCs cost on the wire, and the compiler lists each piece of code that read the old fields. A hash of what the clients read goes into Consul with each world, so that a client stays off a world whose content it doesn't have. Last, a second kind of monster, the Ash Skeleton, joins the Ashlands without a line of code.

Topics: Gameplay

The code

Each step on screen is a tag in the game's repository. The code at the end of the episode is ep027/end.

  1. 3:06ep027/step-1A content folderBrowseChanges
  2. 12:04ep027/step-2Monsters as dataBrowseChanges
  3. 25:49ep027/step-3NPCs as dataBrowseChanges
  4. 30:14ep027/step-4Sent by kindBrowseChanges
  5. 33:15ep027/step-5Drawn by kindBrowseChanges
  6. 39:37ep027/step-6Which contentBrowseChanges
  7. 45:09ep027/step-7A second monsterBrowseChanges
  8. 46:23ep027/step-8DocsBrowseChanges

Transcript

Intro

0:00 Hello and welcome back to episode twenty-seven, where we're going to move the rest of our game's content, the monsters and the NPCs, into data that both the server and the client read, like the sword last time. Let's begin with a quick look at where we're going to be by the end of the episode.

0:17 alice runs through the gate to the Ashlands, past NPCs whose names and looks her client reads from a content folder, by their kind. Around the middle of the Ashlands stand Ash Skeletons, a second kind of monster, which we're going to add without writing a line of code.

0:35 On the website of the series you can find more information about the project. The code for the project is open source on GitHub, tagged at each step of each episode so that you can follow along.

Last time

0:47 Now, before we get to today's work, let's quickly go over what we did last time. Our sword moved out of code, into an items file in protobuf's text format, with a schema that both the server and the client compile in. The parser refuses a field that the schema doesn't have, with its line, and our own checks refuse what types can't catch, such as two items with one id. The server fights with the weapon's numbers from the file, which its image has to carry, as we found out when the server stopped in Compose. The client draws each weapon from its look, and each player holds a weapon by its id, which goes out once a second, and into Redis and MySQL with the rest of the player.

Today

1:25 Today, the items folder becomes a content folder, with a library that reads all of the game's content, as monsters and NPCs move in next to the items. Our monsters' kinds, and where they spawn, move into it, and so do the NPCs. With the content on both sides, the server can send a monster or an NPC by its kind, and the client looks up the name and the look. A client and a world then have to agree on that content, so each world is going to list a hash of it, and a client is going to stay off a world whose content it doesn't have. Last, a second kind of monster joins the Ashlands, as nothing more than an entry in a file. All right, let's get started with where the monsters and the NPCs stand today.

Where things stand

2:08 Since episode twenty-four, the server keeps the kinds of monsters and NPCs, and where they spawn, as tables in its C++ code. The client has its own half of the skeleton in code: the model's file, its height, and how fast it plays its clips. On the wire, a monster has a name but no kind, so the client draws each monster as the skeleton. That means a second kind of monster on the server would still look like a skeleton on the client, as nothing ties the two halves together, which was the sword's problem before last episode. The NPCs are better off, as the server sends each one with its name and its look, a colour and a radius, so the client needs no table. However, that costs bytes in each world they go in, and a model for them later would need more fields on the wire. And each change to a monster or an NPC, even its health, means building the server again. So my proposal is to move them into data, next to the items, with the client's part of each kind in there too.

A content folder

3:06 The items folder becomes content, as the monsters and the NPCs are going to sit next to the items, so items dot proto moves there as it is. The items file moves too, and its header now names the schema at its new path, content slash items dot proto, so that an editor still finds it. Reading the text format, with the line of each error, is going to be the work each file needs, so it gets a header of its own, text dot h. It includes string and string view, Abseil's status and status or, and protobuf's message dot h, for the base class of all of the messages that protoc generates. Parse text takes the text, and a reference to the message to parse it into. The message is a google protobuf message, the base class, so that one function parses items, monsters or NPCs, whatever their generated class, as the text format parser works through the message's descriptor, which knows its fields. Its comment says what it returns when the text is wrong: the line it went wrong on, such as at a field the message doesn't have. Read file reads a whole file into a string, or says why it can't, which was the first half of read items. In text dot c c, the error collector from last time moves over as it was, with the parser, and read file is the reading that read items did, with an input file stream and a string stream. Items dot h keeps what's particular to items, the item table and parse items, and read items goes, as the content library is going to read the files. Its include of the generated header follows the schema to content slash items dot p b dot h. In items dot c c, parse items calls parse text, returns its error if there's one, and then checks the ids and the starting weapon, as it did.

4:53 The library gets a header named after it, content dot h, which is what the server, the client and the bots are going to include, so that the content library is the one place that lists the folder's files. It starts with its header guard, then string, Abseil's status or, and items dot h. Content is a struct for what the game is made of, which the world servers and the clients both read from the content folder, as its comment says. It's a plain struct, with a table for each kind of content, which the parts of the game look things up in. A struct suits it, as the content has nothing to keep consistent between its fields once it's been read and checked, so there's no class with methods to guard it. For now, it has the items, and the monsters and the NPCs are going to join them. Read content takes the folder, and reads each file in it into the content, or says which file is wrong, and where, as its comment says. It returns the content by value, in a status or, so a caller gets content that's been read and checked, or an error, and nothing half-read. Content dot c c starts with its own header, then string, string view and utility, then str cat, and text dot h, for read file. Read, in the unnamed namespace, reads one of the folder's files into its table. It's a template on the table's type, as each kind of content is going to have a table of its own. For each of them, it does three things: it reads the file, parses it, and puts the file's name before its error. Its first parameters are the folder and the file's name, as a const char pointer, for the names we write in the code. Read takes the parse function as a pointer to a function, from a string view to a status or of that table, so the items pass parse items, and the monsters are going to pass theirs. The compiler works out the table's type from the function we pass, so we don't have to write it. Three files make a template worth it, where three copies of a few lines would drift apart the first time one of them changed. The path is the folder, a slash and the file's name, and if the file can't be read, read returns why, with the path already in it. Then it parses the text, and puts the path in front of what's wrong, the way read items did, so that an error says the file and the line. Otherwise, the table moves out of the status or into the content, without a copy of the map. Read content sits outside the unnamed namespace, as it's the function the header declares. It makes an empty content, which each file fills in turn, reads the items file into its items, and returns the error, if there's one, or the content, which the status or takes by moving it. The folder's CMake file is the items library's, renamed to nanommo content, with text dot c c and content dot c c, and a test executable, nanommo content tests, which gets the folder's path as a define. The test moves too, as content test dot c c, where reads the game's content reads the whole folder and checks the sword, and the test of what the items' checks refuse stays as it was. The top CMake file adds content in place of items, and so do the API docs.

8:05 On the server, main includes content dot h in place of items dot h, and reads the content where it read the items. The setting is nanommo content now, which names a folder, and that's content by default, under where the server starts. If the folder can't be read, the server says why and stops, as it did for the items, and create gets the content. In server dot h, content dot h comes in, and create and the constructor take the content where they took the item table, as a const reference, as the server reads it and keeps none of it. In server dot c c, create passes it on to the constructor, which gives each land's world the content's items, as the world fights with them. World dot h includes content slash items dot h, at its new path, and so does world test dot c c, for the tests' sword. The server's library links nanommo content, where it linked nanommo items. In the Dockerfile, the server's stage copies each text proto in the content folder, with a star, into slash u s r slash local slash share slash nanommo slash content, and the setting points at that folder. A new file in the folder then ships without a change to the Dockerfile, which is what bit us last time. The profiling stage gets those two lines too, and the lint stage checks the content folder in place of the items folder.

9:27 The client reads its content once, as it starts, in main, with content dot h. It reads the content folder next to the client, and if it can't, it says why and stops, as a client without its content can't draw the world. Last time, the renderer read the items on its own, and drew nobody holding anything without them. With all of the content behind the drawing now, the client stops instead. The renderer gets the content now, as a constructor argument. In characters dot h, items file becomes content folder, which is the application's folder, and content, as the build copies the files into a folder of their own, next to the client. In renderer dot h, content dot h comes in, and the constructor takes the content as a const reference. An empty content is its default, so that the tests and the bench, which have no weapons to draw, don't need one. It's explicit, like our other constructors with a single argument. In renderer dot c c, the constructor takes the content, and the players' characters hold each of its items, by id, with each item's look. The loop takes each entry of the map with a structured binding, as before, and the comment on the cube's shader goes over the loop, where the reading of the file and its warning were. In the client's CMake file, the library links nanommo content. File glob lists each text proto in the content folder, and configure depends checks that list on each build, so that a new file gets copied without running CMake again. The target that copies the assets makes a content folder next to the client, and copies the files into it, if they're different.

10:58 We build it and run the content tests, and both pass, so the sword reads from its new folder, and the items' checks still refuse what they did.

Monsters as data

11:14 A kind of monster has an id and a name, like an item, with its level, its most health, and the experience a player gets for it. Its fight is the server's part, with its damage and its swing. The fight also holds what were constants for all of the monsters: how far a monster sees, its leash, its reach and how far it wanders, so that each kind gets its own. Its look is the client's part: the model, its height, and a tint, which the client is going to read later in the episode. Spawns and camps say where monsters of a kind stand, and they name their land, which reads better in a file than the index we used in C++. There's also a rule that was a comment in the code: each skeleton's home is further from the middle of its land than its leash, so that players who come back to life in the middle aren't chased. In data, a comment is easy to miss, so the parser is going to check it.

12:04 In monsters dot proto, we use proto three syntax, and the package is nanommo dot monsters, next to the items', so that its messages have names of their own in C++, in the nanommo monsters namespace. A kind there doesn't clash with an NPC's kind, which is going to be in nanommo NPCs. Fight is how a kind of monster fights, and how far it goes, which its comment calls the server's part, like an item's weapon. It has the damage of a blow, the swing, in milliseconds, and the hit, how far into its swing the blow lands. Reach is in metres, like the rest of them, as the comment says. Then there's sight, how close a player comes before it gives chase, the leash, how far from its home it gives up on a player, and wander, how far from its home it walks around. As fields of the fight, those four can differ from one kind of monster to the next. Look is the client's part, with a glTF file in the client's monsters folder, its height, in metres, and a tint, as red, green, blue and alpha. The tint is optional, a proto three keyword that keeps track of whether a scalar field was set, so that the generated class gets has tint. Without it, a look with no tint would read as zero, which is transparent black, while a look without one should keep the model's own colours, as the comment says. A kind has an id, from one, which it keeps for good, a name, a level, its most health, and the experience a player gets for killing one, then its fight and its look. A spawn is a monster whose home stays in one place, in metres from the middle of its land, with its kind, by id, its land, by name, and its x and z. A name also survives a new land added in front of the others, where an index would point at the wrong land. Camps are camps of monsters of a kind, so they have a kind, a land and a count, and no places, as the camps function from episode fifteen picks those from the land's seed. That way, a new world with another seed gets its camps in places that suit its island. Monsters is the whole file, with repeated kinds, spawns and camps. In the text format, each field goes by its name, while on the wire it goes by its number. So renaming a field would break our text files, and renumbering it would break anything stored in binary, which is why we pick both once. Monsters dot t x t p b starts with the header for that schema. The skeleton is kind one, with the numbers from the C++ table and the shared constants: level one, fifty health, fifty experience and five damage. Its swing takes nine hundred milliseconds and hits at six hundred and fifty, with a reach of one point two metres, three metres of sight, a leash of four, and three metres of wandering. Its look is skeleton dot g l b, at one point six metres, with no tint. Each spawn takes a single line, three in the Meadows and three in the Ashlands, where the table had them, and each land gets eight camps of skeletons.

14:58 Monster kinds need the checks on ids that items have, so those move into a header of checks that more than one kind of content needs, checks dot h. It includes Abseil's status, status or and str cat, protobuf's repeated pointer field, which is the type of a repeated message field, and our lands. By id is a template, for items, or kinds of monster or NPC, as each of them has an id and a name, while each is a class of its own, generated from its own schema. The template works with any type that has those two getters. It takes the repeated field from a file, and returns a map by id, or what's wrong with an id. Its comment lists what can be wrong: an id of zero, which is what protobuf gives a field we leave out, or one that two of them have. A template goes in a header, where the compiler sees its body for each type we use it with. So we type the body here, where a plain function's body would go in a dot c c file. The loop is the one from parse items, with the entry's name in the error, and emplace saying when an id is taken. The map comes back by value, which costs no copy, as C++ moves a local out of a function when it returns it, and the status or takes it from there. Check spawn is a template on the kind and the spawn, and its comment says what it checks: whether a spawn names a kind we have, in a land we have. Monster spawns, camps and NPC spawns all have a kind and a land, so one function checks all three. It looks for the spawn's kind in the map, with contains, from C++20, and for its land, by name, with is land, and returns an error that names the one it didn't find, or OK when it found both. Items dot c c is the first to use by id. Map and utility come in, with checks dot h, which takes the place of str cat here, and the loop over the items goes, and parse items takes its map from by id. It returns the error, if there's one, and the item table starts with the map, moved in, and the starting weapon. In lands dot h, is land says whether a land has that name, by going through k lands. We already had land named, which returns the Meadows for a name it doesn't know, and that suits a saved player whose land has gone, while a typo in a file should be an error. The monsters get a header and a source file in the library, next to the items'. Monsters dot h includes map, string view and vector, Abseil's status or, and the generated monsters dot p b dot h, which protoc writes from our schema. It has the monster table, with the kinds by id, and the spawns and camps as vectors, as nothing looks them up: the server goes through them once, as it starts. We keep the generated classes in the table, with no structs of our own to copy them into, so a new field in the schema reaches the code that wants it without a change here. Parse monsters returns the table, or what's wrong with the file, and its comment lists the problems it looks for. A file can hold a field the schema doesn't have, or a kind with no id or with another's. A spawn or camps can name a kind or a land that isn't there, and a spawn's leash can reach the middle of its land. Whoever edits the file can read that list to see what the parser is going to hold them to. Monsters dot c c starts with its own header, then c math, for the distance from the middle, and c s t d int, map, string view and utility. From Abseil, it takes str cat for the errors, and from our own code, checks dot h and text dot h, so it reads a lot like items dot c c, with more to check. Parse monsters makes an empty monsters message, the whole file, and parses the text into it with parse text. It returns the error if there's one, and takes its kinds by id. With the kinds in hand, it makes the table, with the kinds, and the spawns and camps copied out of their repeated fields into vectors, from the fields' begin and end. A repeated field already works like a container, so the vector copies straight from it. Each spawn goes through check spawn, which returns the first problem it finds, and so does the loop. Then comes the leash: we take the spawn's kind, which is in the table, as check spawn just said so. Over that line goes the reason for the check, as a comment: players come back to life in the middle of their land, so a monster can't chase them there. If its home is within its leash of the middle, it's an error, which says which monster, where, and in which land, so that whoever edited the file can find the line they broke. Str cat builds it from the kind's name, the spawn's place and its land. Episode eleven kept that rule in a comment over k leash, and a number in a file has nothing like it next to the code that depends on it, so the parser checks the rule instead. Camps go through check spawn too, for their kind and their land, with no leash check, as a camp has no place in the file to check. They stand at least thirty metres from the middle, where no leash reaches. In content dot h, the content gets the monsters, and read content reads monsters dot t x t p b once the items have read without an error. The content library gets the new proto and monsters dot c c, and links our terrain library, for the lands.

20:14 On the server, we start with the monster struct, in world dot h, which gets its kind, by its id in the monsters file, as its comment says, and the wire is going to need it later. It gets a reach too, in metres, from its kind's fight, as the comment over it says, and then three more numbers from the fight, its sight, its leash and its wander. A monster copies them from its kind when it spawns, like its damage and its swing, so the code that runs monsters reads them from the monster in front of it, and doesn't look up a kind on each tick. In monsters dot h, the content's monsters dot h comes in, and the monster kind struct, the k skeleton table, the spawns, the camps and the four constants go, as the file has all of them now. The two rests stay, as all kinds rest alike for now. Monster of takes the generated kind, and spawn monsters takes the table, as its comment says now: it spawns the land's monsters from the table's spawns and camps, by the land's index. In monsters dot c c, lands dot h comes in, for land named, as the file names each spawn's land, and the server knows a land by its place in k lands. A monster picks the place it walks to within its own wander. Monster of takes a reference to the kind's fight, and fills in the monster with the kind's id, and its level, health and experience. Those are cast to int where the monster keeps an int, as the file's numbers are unsigned, and a level can't be negative. The swing and the hit become chrono milliseconds, and the reach, sight, leash and wander come over as they are. Spawn monsters takes the table now, where it read the C++ tables. It goes through the table's spawns, and spawns the ones whose land's name gives this land's index, with the kind from the table. At would throw for a kind that isn't there, and the guide doesn't use exceptions, but here it can't happen, as the parser checked each spawn's kind before the server got the table. Camps work like that too, with the camp's kind looked up once, for its three monsters, and the count cast to the int that camps takes. The functions that used the constants come next, where spot looks as far as the monster's sight, and leaves a player alone past its leash, and hunt swings within its reach, and stops that far from its prey. In world dot c c, a monster's blow lands within its own reach too, where it used the constant. In server dot h, start takes the content, as that's where the server spawns its monsters. And in server dot c c, create passes the content to start, which spawns each land's monsters from the content's table, by the land's index.

22:47 Our tests used the C++ table's skeleton, which is gone now, so they need a skeleton of their own. The world's tests and the monsters' tests are going to share it, in a header for the tests, test content dot h, next to test services dot h, in the nanommo testing namespace. It includes the content's items and monsters headers, for parse items and parse monsters. The comment says why the tests keep content of their own: so that the game's content can change without breaking them, which matters more now that changing the content is an edit to a text file. The sword from last episode moves here from world test, so that the tests' content sits in one header. Skeleton parses a skeleton from text, with the numbers that the tests were written for, a level, fifty health, fifty experience, and a fight with the skeleton's damage, swing, reach, sight, leash and wander, and returns its kind one. Both functions are inline, as they're defined in a header. In world test dot c c, test content dot h comes in, and the tests' sword goes from the top of the file. Each test that makes a monster now makes it from the tests' skeleton, in the nanommo testing namespace. Those are the tests that spawn monsters with ids of their own, the ones that swing at the nearest monster and kill one for its experience, and the one where a skeleton kills a player. The fights take the sword from test content too, where it moved. None of their checks change, as the tests' skeleton has the numbers that the C++ table had. In monsters test dot c c, each skeleton comes from there too. There's the one that rests and then walks, the one that stays near home in a corner of the board, the one that runs at a player and swings, and the one that spots players near its home. The checks that used the constants, the wander's radius and the reach, use the monster's own, as the monster copied them from its kind, so the tests check what the monster does with the numbers it has. In content test dot c c, reads the game's content now finds the skeleton in the real file, by its id, one, checks its name, and that the file has spawns. The new test is what the monsters' checks refuse, with a monster file of one line each time, and each check in the test has something different wrong with it, so a failure points at one check. A kind with a spawn five metres from the middle of the Meadows is fine. That tells us the test's text is valid to begin with. Then a spawn of a kind we don't have is refused, and so are camps in a land called the Marsh, which we don't have. Last, a monster five metres from the middle of its land, with a leash of six, is refused, as it could chase players where they come back to life.

25:28 The content tests pass, with the monsters' checks, and so do the server's tests of the world and the monsters, so a skeleton parsed from text behaves like the C++ table's did.

NPCs as data

25:49 The NPCs follow, in npcs dot proto, in a package of their own, nanommo dot npcs, so that an NPC's kind and look have names of their own in C++. An NPC's look is a ball, until NPCs have models, as the comment says, with a colour, as red, green, blue and alpha in a single number, as raylib's get colour reads them, and a radius, in metres. When NPCs get models, their look is going to gain a model, like a monster's, which is a change to the schema and the client, and none to the wire. A kind has an id, from one, which it keeps for good, and a name, and whether players can talk to it, as its comment says: an NPC that talks tells stories. Then it has its look, the ball with its colour and radius. A spawn is where an NPC stands, like a monster's spawn, with one more field, with its kind, its land by name, its place, and which way it faces, in radians, as the comment over it says. The whole file has repeated kinds and spawns, like the monsters' file without the camps, as NPCs don't come in groups. In npcs dot t x t p b, the five kinds come from the C++ table: the guard, the merchant, the healer, the ash warden and the storyteller, which is the one that talks. Each look takes a single line, with its colour in hex, as the text format reads hex for an integer field, which keeps the colours as they were. The eight spawns each take a line too, four in each land, where the table had them, and a yaw of one point five seven is a quarter turn.

27:19 The NPCs' header in the library, npcs dot h, has the NPC table, with the kinds by id and the spawns, and parse NPCs, with what it refuses in its comment. In npcs dot c c, parse NPCs is the monsters' parser with less to check: the kinds by id, the table, and check spawn for each spawn, so we paste it. An NPC doesn't chase anyone, so it has no leash to check. The content gets the NPCs, next to the monsters, and read content reads npcs dot t x t p b after the monsters. The library gets the proto and npcs dot c c, and the generated code's warnings stay off for it too. In content test dot c c, algorithm comes in, for any of, from the ranges library. Reads the game's content checks that one of the kinds talks, as our storyteller needs one. A lambda takes each entry of the map, a pair of the id and the kind, and asks the kind whether it talks. And refuses NPCs that don't add up has a spawn of a kind we don't have, which check spawn refuses. On the server, npcs dot h loses its tables to the file: the NPC kind struct, the five kinds and the spawns. It includes the content's npcs dot h instead, for the table, in place of array and c s t d int. Spawn NPCs takes the table, as its comment says now: it spawns the land's NPCs from the table, where the ground is walkable. In npcs dot c c, lands dot h comes in, for land named, as the spawns name their land, spawn NPCs goes through the table's spawns, skips the other lands' by name, and takes each spawn's kind from the table. The rest stays as it was: an NPC whose ground isn't walkable is left out, with a line that says so, now through the generated getters. Each NPC gets its kind's id, with the kind's name, look and whether it talks. In world dot h, an NPC gets its kind, by its id in the NPCs file, and the server's start spawns each land's NPCs from the content, where it spawns their monsters.

29:26 The content tests pass, with the NPCs read from their file, and a spawn of a missing kind refused.

Sent by kind

29:42 With the content on both sides, the server can send an NPC by its kind's id, and leave out its name, colour and radius, which the client looks up. The guard goes from thirty-two bytes in a world to sixteen, which is its id, its place and its kind. The Meadows' four NPCs go from one hundred and fifty bytes to seventy-four, for each player near them, once a second. A monster's name, which the server sends once a second as well, goes from ten bytes for a skeleton to two for its kind. The bigger win is that a new kind, with its own model, needs no change to the wire.

30:15 In nanommo dot proto, the monster's name goes, as the client is going to look it up by the monster's kind. Its field number, two, becomes reserved, and so does the name, with the reserved keyword. The comment says why: a monster's name comes from its kind now, and a number we've used stays retired. That way, an old client doesn't read a new field as the old one, and protoc refuses a new field that takes it. The monster gets its kind, as field ten, by its id in the monsters file. The NPC's comment says it comes once a second with its kind now, whose name and look are in the NPCs file. Its name, colour, radius and whether it talks go, and their numbers are reserved, two, and six to eight. Their names are reserved too, as a JSON or text format reader goes by the name, where the binary goes by the number. Then the NPC gets its kind, as field number nine. In parts dot c c, update keeps what a world leaves out between the full worlds, and its comment gets kinds. A monster has no name now, so the name goes inside the if constexpr for players, with the weapon. An else, for monsters, keeps the kind, so that a monster stays the kind it was between the worlds that send it. In server dot c c, to protocol sets a monster's kind where it set its name, from the kind the monster was spawned with, and an NPC's kind where it set its name and look. The comment in broadcast gets kinds too, next to the names, levels and weapons, which change now and then, so they go out once a second. On the worlds in between, a monster's kind is cleared, like a name was. In world dot h, a monster loses its name, and an NPC its colour and radius, as the server doesn't use them. An NPC keeps its name and whether it talks, for the storyteller. Monster of stops copying the kind's name into the monster, as nothing on the server reads it, and spawn NPCs stops copying the look into the NPC, as nothing on the server reads it either. The protocol test's world has a monster with a kind in place of a name, and checks that the kind survives the round trip, one, as an unsigned number. And the parts test's guard, which parts keeps between the worlds that send it, has kind one in place of its name, which the test checks is still there two seconds later.

32:33 Let's build it, and see what the compiler makes of that. We run the build in a container from the toolchain stage of our Dockerfile, and grep for the errors. Ninja gets dash k zero, which keeps it going after a file fails, so that we see all of the failures at once. The client's animator reads each monster's name, and each NPC's name, colour, radius and whether it talks, from their messages, and so does its test. And the bot reads whether an NPC talks, to find the storyteller. When a field leaves the schema, its getter goes, so each piece of code that read it fails to compile, and the build lists them for us. Both the client and the bot are going to read the kinds from the content.

Drawn by kind

33:15 The animator turns the world into figures to draw, so that's where a kind turns into a name and a look. In animator dot h, utility and content dot h come in. A figure gets its kind, a monster's or an NPC's, by its id in the content folder, and the comment on its colour says that a monster is tinted this colour. The animator takes the content, by value, and moves it into a member, with the reason in the comment: monsters and NPCs come by kind, so the animator looks up their names and looks in the content. It keeps a copy, so that it can't outlive what it reads, and the content is small. In animator dot c c, follow stops copying the name, as a monster has none on the wire. Stand takes the NPC table, gives the figure its kind, and finds the kind in the table, with find, as weapon of does on the server. The table is const, so the square brackets, which add a missing key, wouldn't compile here. If the client has it, the figure gets the kind's name, its colour, which get colour reads from the number, its radius, and whether it talks. The picking from episode twenty-five reads that flag, to decide what a click can talk to. The comment says what happens to a kind from a newer server: it stays nameless, and with a radius of zero, too small to see. In update, a player's name comes from its message, as before. A monster gets its kind, for the renderer, which picks its model by kind. If the content has the kind, the monster gets its name, which the nameplates show, and its tint if the kind has one, which is where has tint comes in, or white, which leaves the model's colours as they are. Stand gets the content's NPC table, as a const reference, so nothing is copied for each NPC on each frame. In renderer dot h, c s t d int, map and memory come in, for the monsters. The monsters become a map from each kind's id to its characters, its model at its height, held by a unique pointer, as characters can't be copied or moved. In renderer dot c c, memory and string come in, for make unique and the model's file name. The monsters' member goes from the constructor's list, as the map starts empty, and the constructor loads each kind's model, from the monsters folder, into characters of its own. Characters takes a list of files, as the players pick from two, so a kind passes a vector with the one file its look names, at the kind's height. The comment over the loop says what that costs: two kinds with one model load it twice, which is fine for a few kinds. Sharing a model between kinds would mean a second map, of models by file, which we can add when there are enough kinds for it to matter. In draw figure, a monster finds the characters of its kind. It gets none when the kind isn't in our content, as a null pointer, which is why looks is a pointer. A monster is drawn in its figure's colour, which is its kind's tint, or white, where players are drawn in their own colour. A kind we don't have, with no characters, falls through to the cube, which is how the client drew players before they had models. In main, the animator gets the content, which it copies, so main keeps its own, which the renderer reads too. The animator test's monster has kind one, in a content with a skeleton, from text, and the test checks that the figure gets the name skeleton, which now comes from its kind. The test that draws the skeleton with its arms down needs the renderer to know the skeleton's kind now, or it would draw a cube. So the test gives the renderer a content with a kind from text, in a raw string, like the tests' sword. Kind one has no name, as the test looks at pixels and not at nameplates. The kind has a look that names skeleton dot g l b at one point six metres, on a single line. The renderer gets that content, and the figure that it draws is a monster of kind one, so that the renderer draws it with that look.

37:02 The bots read the content too, for the NPCs that talk. In bot dot h, npcs dot h comes in, and the constructor takes the NPC table, with a comment: it finds the NPCs that talk through their kinds in the table. The bot keeps a const reference, as main keeps the content for as long as the bots run. In bot dot c c, the constructor stores the reference, after udp, in the order of the members, and an NPC is the storyteller when its kind is in the table and talks. In the bot's main, content dot h comes in, next to our other headers. The swarm makes the bots as they log in, a few a second, over the ramp we give it. It takes the table now, and passes it to each bot it makes. Main reads the content from the nanommo content setting, like the server, with content as its default, which works when the bots start in the repo. If it can't read it, it says why, in the bot's words, and stops. The content lives in main until the bots stop, which is what lets each bot keep a reference to its NPCs, and the swarm gets them from the content. The bot library links nanommo content, for read content and the NPC table. The bot's image copies the content folder and sets the setting, like the server's, as bots run in Compose for our load tests.

38:19 Now it builds, so let's run it, in the Meadows first. Once alice is in, the guard and the storyteller stand in front of her, where they did, in their colours, with their names over them, which the client looked up by their kinds. Walking up the road to the gate, the world looks as it did, and turning the camera shows the skeletons around the middle, with their names, from their kind too.

38:42 The animator's tests pass too, with the skeleton's name from its kind.

Which content

38:51 Each side reads its own copy of the content, the server from its image and the client from next to it. A client whose content differs from its world's would draw a kind it doesn't have as a cube, or a tinted skeleton as a plain one, and the player would have no idea why. So each world is going to work out a hash of the content that the clients read, the ids, names and looks, and a client is going to compare it with its own. The fights stay out of the hash, so that changing a sword's damage, or a skeleton's leash, needs a restart of the worlds and no new client, which is how most balance changes go. Each world registers its hash with Consul, next to its players, and the login server lists it with the world, so the client can tell, before it logs in, which worlds have its content, and stay off the others.

39:38 In content dot h, c s t d int comes in, for client hash, which returns a uint64. Its comment says what it is: a fingerprint of what the clients read, so that a client can tell whether a world has its content. What the server alone reads, such as a weapon's damage, stays out, so that changing it doesn't need a new client. In content dot c c, we include protobuf's coded stream and its string output stream, as we're going to serialise with options. The hash is FNV dash one A, sixty-four bits wide, with its two constants, the offset basis it starts from, and the prime. The comment says why we don't take std hash or Abseil's hash, which are both quicker to write. The standard asks std hash to be stable within one run of a program and no more, and Abseil's hash changes from run to run on purpose. Our hash has to match between a server on Linux and a client on a Mac. FNV dash one A is a few lines, and it's what our terrain test used to fingerprint an island. Add serialises a message into a string, through a string output stream, and a coded output stream on top of it, which can serialise deterministically. As the comment says, that's so that equal messages give equal bytes. A map field is where protobuf leaves the order open, and none of these messages has one yet, so this keeps the hash stable for a map we add later. The braces around them end the streams there, which flushes the bytes into the string. Then each byte goes into the hash, xor first, then multiplied by the prime, which is what the A in FNV dash one A means. Client hash starts from the offset basis, and goes through the items by id, so in a fixed order. It makes a copy of each with what the clients read, as the comment says, its id, its name and its look, and no weapon. Copying in what the clients read, where we could have cleared what they don't, means that a field we add to a weapon later stays out of the hash. A kind of monster gives its id, name and look, without its level, health, experience or fight, and an NPC's kind gives its id, name, whether it talks, and its look, all of which the client reads. Last, it returns the hash, a single sixty-four bit number for all of it. In content test dot c c, c s t d int comes in, and hashes what the clients read reads the game's content and takes its hash, which is what the test compares against. A sword with a thousand damage leaves the hash as it was, while a sword two metres long changes it.

42:08 In nanommo dot proto, a world listing gets content, as field five, a fixed64. The comment says why it's not a varint, like the others: it's the world's client hash, whose high bits are as likely set as not. So it takes eight bytes as a fixed64, where a varint would take up to ten. In consul dot h, a world's registration gets its content, as the client hash of the world's content. In consul dot c c, register puts it in the service's meta, next to the players, as sixteen hex digits, with Abseil's hex, padded with zeros, as Consul's meta holds strings. Worlds finds it in each entry, with find, from episode twenty-one, which returns null when it's missing or isn't a string, and if it's there and reads as hex, the listing gets it. The comment says what happens to a world from before content hashes: it lists none, a zero, which no client has. In consul test dot c c, the world registers with a content, which comes back in its listing, with the high bit set. In the server's main, str cat comes in, and once the server is listening, it works out its content's hash, and prints it, and the registration gets it.

43:20 On the client, the login screen gets the client hash of the client's content, to tell the worlds that have other content apart, as its comment says. The constructor is explicit, and it keeps the hash in content underscore, for each time the list is drawn. Main works out the hash from the content, so the login screen needs the number and nothing more. In login screen dot c c, a world with other content gets a note next to its players in the list, other content, with a warning icon, in grey. When we pick a world, one with other content shows an error, and we stay on the list. The comment gives the reason: a client draws what the world sends from its own content, so with other content, it would draw the wrong things, or nothing. The error names the world: it has other content than this client, so they can't play together. Otherwise, we go on to the login, as before, and the error is cleared. In main, the client works out its hash, prints it as it starts, like the server, so the two can be compared by eye, and the login screen gets it.

44:22 Let's try it with two worlds: Ember, as it is, and Frost, which runs from a copy of the content where the sword is half a metre longer, as if it had been updated and our client hadn't. The list shows both worlds, and Frost with other content next to it. Picking Frost keeps us on the list, with the error under it, which says that Frost has other content than this client. Ember has ours, so it takes us to the login, and into the game.

44:51 The Consul test needs Consul, so Compose starts it, with what the tests depend on, and the content comes back in the world's listing. And the hash leaves the sword's damage out, while its length goes in.

A second monster

45:09 With all of that in place, a second kind of monster is an entry in monsters dot t x t p b, and nothing else. The Ash Skeleton is kind two, at level two, with eighty health, and ninety experience for killing one. It hits for eight, with the skeleton's swing, as it uses the skeleton's model, and the swing's timing follows the attack animation. That's a link the file can't check, so a kind with another model would need its own times. It sees a metre further than a skeleton, while its leash stays at four, as the Ashlands' monsters with a home stand four and a bit metres from the middle. A leash that long or longer would get refused by the check we wrote. Its look is the skeleton's model, at its height, tinted the orange of the lava. The three monsters with a home in the Ashlands become Ash Skeletons, by their kind's id, two, and so do its camps.

46:01 Let's build it and run it, without a change to the code. alice runs through the gate into the Ashlands, and arrives facing away from the middle, so we turn the camera around. Walking back towards the middle, the Ash Skeletons stand around it, orange, with their names and level two over them, and the client changed nothing but its content.

Docs

46:23 Last, the docs, starting with the monsters' README, next to the skeleton's model in the assets. It said that the client scales the skeleton to one point six metres, which is the kind's to say now. So it says that the client scales it to the height of each kind whose look names it, in the content's monsters file. The weapons' README says that the sword's length and grip are in content slash items dot t x t p b. In the main README, the section on items becomes a section on content. It covers the three files in the content folder, which the world servers, the client and the bots read. Then it explains which parts of an item or a kind are the server's and which the client's, what the parser checks and what our own checks add, and why ids start from one. And it says that monsters and NPCs go out by kind, with the old fields reserved, and how the content hash keeps a client off a world with other content. The sections on monsters, fighting and NPCs point at the files, with the Ash Skeleton's numbers and the NPCs' bytes by kind, and the worlds' settings list nanommo content in place of nanommo items, with content as its default. The new section ends on what a change takes: an edit to a file and a restart of the worlds, and a new client when the change is to what the clients read, as the Ash Skeleton's was.

Wrap-up

47:44 So, today the items folder became a content folder, with a library that reads all of the game's content, and that the world servers, the clients and the bots link. The kinds of monsters and NPCs, and where they stand, moved into it, out of C++ tables. Our own checks catch what the types can't, such as a spawn of a kind we don't have, or a leash that reaches the middle of a land. Monsters and NPCs go to the players by kind now, which halves what the NPCs cost, after the compiler listed each piece of code that read the fields we took out. Each world lists a hash of what the clients read, through Consul, and a client stays off a world whose content it doesn't have. And the Ash Skeletons joined the Ashlands as an entry in a file.

Playing it

49:13 Let me know in the comments what you'd like to see next. And if you want to follow along with the series, subscribe to the channel. I hope you find this useful, and thanks for watching.