nanoMMO

A data-driven item system for an MMO game server

Items made of parts instead of types, with stacks, equipment slots, stats, potions and rolls, and a bag to carry them in, which a property test plays with twenty thousand times.

Watch on YouTube

We design the item system of our MMORPG as data: an item is the parts it has, such as how many stack, where it's worn and what it adds, what using one does and what each one rolls, instead of a type in the code, and the content's checks refuse parts that don't go together. Then a bag of twenty-four slots, with a slot for each piece of equipment, which stacks, splits, swaps and wears items, and a property test that makes twenty thousand random moves and checks that no item is ever made or lost. This is episode 39 of a series about building an MMORPG from scratch in C++, with the focus on the engineering around the game.

The code

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

  1. 2:19ep039/step-1One copy of the contentBrowseChanges
  2. 6:21ep039/step-2Items made of partsBrowseChanges
  3. 18:18ep039/step-3A bagBrowseChanges

Transcript

Intro

0:00 Hello and welcome back to episode thirty-nine, where we're going to make items into data that the game reads, and give the server a bag to carry them in. Let's begin with a quick look at where we're going to be by the end of the episode.

0:13 Our items file has seven items now, made of parts that say how they stack, where they're worn and what they add, and the server has a bag of twenty-four slots to carry them in. Its tests pass, and the last of them moves items around a bag twenty thousand times at random, and checks that no item is ever made or lost.

0:32 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:43 Now, before we get to today's work, let's quickly go over what we did last time. The world server ran its lands as copies, each with an id, so that a land can have more than one. The gates got a list of their own, each with a twin that leads back, so that the Meadows lead to the Ashlands and to a dungeon, the Crypt. Each player who goes into the Crypt gets a copy of it of their own, with its own skeletons. A copy of the Crypt with nobody in it stops, and goes after five minutes, and players in the Crypt are saved outside it, so that a logout or a deploy brings them back to the Meadows.

Today

1:20 Since episode twenty-six, our game has had one item, the sword, which each player holds, and nothing gives a player anything else. An MMO lives on its items, so we're going to build an item system that can grow to hundreds of them without a change to the code for each new one. First, the world server is going to keep one copy of the content, as each copy of the Crypt has a copy of the items and the skills for now. Then an item is going to be the parts it has, and not a type that the code has to know about. Its parts say how many go in a stack, where it's worn, what it adds and what using one does. The content's checks are going to refuse parts that don't go together, like a weapon that isn't held in the hand. Then the server gets a bag, which holds stacks of items, and the items a player wears, and moves them around. And to trust it with players' items, we're going to test it with thousands of random moves, which check that no item is ever made or lost. All right, let's get started, with the content.

One copy of the content

2:20 Each copy of a land has a World, and since episode thirty-eight, each player who goes into the Crypt gets a copy of it. Each of those Worlds was built with a copy of the items and the skills, so a hundred players in the Crypt meant a hundred copies of the items file in memory, and that file is about to grow. The content doesn't change while the game runs, so the copies can share one. In world dot h, memory comes in, the standard library's header for its smart pointers, like the shared pointers the server keeps its sessions in. Items dot h and skills dot h make way for content dot h, which has all of the content. The constructor takes a shared pointer to the content, where it took the items and the skills. A shared pointer counts the owners of what it points at, and deletes it when the last one lets go, so the content lives as long as any World that uses it. It points at a const Content, so that no World can change what the others read. Without one, a World gets empty content, which make shared makes, for the tests' worlds that have no items. Players fight with the content's weapons and skills, so in a world without content, nobody can. In the private part, the two tables make way for the pointer, to all of the content, so that a World can read any of it. In world dot c c, memory comes in too, and the constructor moves the shared pointer into its member. It takes the pointer by value and moves it, which is how the guide passes something a function keeps, as the caller can move its own pointer in, or copy one. Moving it hands the ownership over without touching the count, where copying it would add an owner, and take one away again a moment later. Then each place that read the two tables reads them through the pointer, with an arrow. An arrow goes through a pointer to what it points at, as a dot does for an object we hold, and nothing else in those functions changes. Join gives a new player the starting weapon and the starting skills, learning a skill looks it up in the skills, and so does a skill as it's used. Weapon of looks up the weapon a player holds, and usable checks whether a player can use a skill. In copies dot h, memory comes in, and copies keeps its content in a shared pointer, where it kept a Content of its own: one copy, which each copy of each land shares. In copies dot c c, the constructor makes it from the content it's given, with make shared, which puts the content and its count in a single allocation. Each new copy's World gets the pointer, which adds an owner and copies nothing of the content, and spawning a land's monsters and NPCs reads them through the pointer. The world tests built their worlds from the items and the skills, so in test content dot h, memory, utility and content dot h come in. With makes content out of the items and skills it's given, in a shared pointer, for a World. It's inline, as it's defined in a header that more than one test file includes, and the skills are empty unless a test gives some. Designated initialisers name the fields we fill, so the monsters and the NPCs stay empty, and With gives a World these items and skills and nothing else. In the world tests, each world that had the sword, or the sword and the kick, gets them through with. Each test still says what's in its world and nothing more, as the tests' content is their own, apart from the game's, so that the game's content can change without breaking them. Two of the worlds fight with the sword, and two of them have the kick as well, for the tests of skills. And the world fuzzer makes its island's content once, in a shared pointer. The pointer in the lambda goes when the lambda returns, and the island's World keeps the content alive, as it holds an owner of its own. So the copy of the island that each input plays on shares the island's content, without a copy of it.

6:05 First, Docker builds the game and its tests at this step, and the build is sped up here. The world's tests and the copies' tests don't need MySQL, Redis or Consul, so dash dash no deps runs them without starting those, and they all pass, with the copies sharing one content.

Items made of parts

6:22 An item is going to be the parts it has, so we start with the schema of the items file, in items dot proto. A slot is where on a player an item's worn, and protobuf's enum gives us a type with named values for it. Protobuf's style puts the enum's name in front of each of its values, as the values of all the enums in a package share one set of names. In proto three, the first value has to be zero, and zero is what a field we leave out reads as, so slot none means the item isn't worn anywhere. Then come the main hand, the off hand, the head, the body and the feet, and each player wears one item in each. A stat is what an item can add to while a player wears it, with none as its zero too. Damage goes on each blow the player lands, with a weapon or a skill, and health on the most health the player can have. Armour is going to come off each blow a monster lands on the player, down to one, so that a blow still does some damage. A modifier is a stat and an amount, which is a signed int thirty-two, so that an amount can go below zero. The weapon stays as it was, and equip is where an item's worn, with the modifiers it adds while it is. A roll is a modifier that each one of an item gets as it's made, with an amount from min to max, so that two swords of one kind can come out different. Use is what using one does, and each use uses one up. For now, that's how much it heals, and how long until the player can use another of the item, in milliseconds, so that nobody drinks a stack of potions in a single second. In look, the model is for what's held in the hand, and most items aren't, so they don't have one. Each item gets an icon instead, for the bag, as a Font Awesome code point, like the skills on the hotbar. And it gets a tint, as red, green, blue and alpha in one number, which goes over the model's own colours and the icon's. It's optional, like the monsters' tint, so that we can tell no tint from a tint of zero, which would be black and see-through. Then the item gets its new parts, starting with stack, which is how many go in one slot of a bag. Without one, an item takes a slot each. Then equip, use and the rolls, each a part that an item has or doesn't. The server and the client are going to read an item by those: an item is the parts it has, and not a type. A potion is something with a use that stacks, and a sword is a weapon that's worn in the hand, so a new item that puts parts we have together needs no new code. A stack is one of an item, or a stack of them: the item by its id, how many, and the modifiers this one rolled. We're going to use it for what a new player starts with, and for what players carry. The starting weapon goes, as a new player is going to start with a kit, and its number and its name are reserved, like the fields the protocol stopped using in episode twenty-seven. That way, no later field can take them. The kit is a list of stacks, which a new player is going to wear where they can, and carry in their bag otherwise. And retired lists the ids of items that are gone. Players may still have them saved, so no new item can take one of those ids, or the old items would turn into the new one. In the items file, the kit is the sword in hand, and three potions for the bag. The sword is worn in the main hand, and Font Awesome's free icons don't have a sword, so it gets the slash, which looks like a blade. The health potion stacks to ten, and heals forty, with ten seconds before you can drink another, and its icon is a flask, tinted red. A bone stacks to fifty, and does nothing, as a monster's leftovers do, so that something in the file piles up in a bag. The leather cap is worn on the head, for one armour, which is going to take a skeleton's blow of five down to four. The leather tunic is worn on the body, for one armour and ten health, and both are tinted a leather brown. The Ash Blade is the Ashlands' sword, which hits for twenty where the sword hits for fifteen, and reaches a little further. Each one rolls up to five more damage of its own. It uses the sword's model, a fifth longer, and tinted in the Ashlands' ember. And the Ash Buckler goes in the off hand, for one armour and up to two more that it rolls. Nobody gets the cap, the tunic or the Ashlands' gear yet, but the checks are going to read each of them as the servers and the client start. In items dot h, vector comes in, for the kit, which the table keeps in a vector as it's read. Players don't carry a bag yet, so the starting weapon stays in the table for now. The starting weapon is the first weapon in the kit, which each player holds until they have a bag to keep the rest in. Parse items now refuses an item with parts that don't go together, such as a weapon that isn't held in the main hand, or a kit with an item the file doesn't have. Stack size gives how many of an item go in one slot of a bag, and count of how many are in a stack, which is one when it doesn't say. And slot name gives a slot's name the way a player would say it, such as main hand, for messages to the player. In items dot c c, algorithm comes in, for std max and ranges find, string for an item's name, and str cat, abseil's way of joining text, for the messages. Check parts finds what one item's parts can't be together, or what's wrong with one of them, and returns it as an error with a message that names the item. It's in an unnamed namespace, as nothing outside this file needs it. Each check returns an invalid argument error, abseil's status for input that's wrong, as the file is our input here. Protobuf generates a has function for each message field, which says whether it's set, and that's how the code asks an item for its parts. So a weapon has to be held in the main hand, as that's the hand the client draws it in, and the one a player is going to swing it with. Something that's worn needs a slot to be worn in, as a slot left out reads as none, and an equip with no slot would add its modifiers from nowhere. Stack size gives one for an item that doesn't say, so anything more is an item that piles up in a slot of the bag. A pile is many of one item, all alike, and the bag is going to keep a pile as one entry with a count, which has no room for the rolls of each one. So an item that stacks isn't allowed to be worn, or roll its own. Worn items, and items that roll, are each one of a kind, which a stack of them can't be. Each modifier has to add to a stat, as a modifier on none adds nothing and is a mistake in the file, and each roll too, from less to more. Its message names the stat with stat name, which protobuf generates for each enum, so the message reads the stat the way the file writes it. Each check returns at the first problem it finds, so a file with two mistakes shows them one at a time, like a compiler's first error. Parse items checks each item's parts as it goes through them by id, and refuses an item whose id is retired. Ranges find looks for the id in the list of retired ids, and returns the list's end when it isn't there, so anything but the end means the id is retired. The table takes the kit from the file, and the check that the starting weapon is a weapon goes. Instead, each stack in the kit has to be of an item the file has, which find looks up in the table by its id. The error for one that isn't there names the id, and the kit can't start a player with more of it than a slot of the bag holds. So three potions are fine, as they stack to ten, and a stack of two swords isn't, as a sword takes a slot of its own, and the message says which item doesn't fit. The first weapon in the kit becomes the starting weapon. Stack size is the item's stack, and one at least, as zero is what we get when the file doesn't say, and count of does that for a stack's count. Both keep the items file short, as most items take a slot each, and most stacks in a kit have a single item in them, so neither needs saying there. Slot name is a switch over the slots, which returns the name of each the way a player would say it: the main hand, the off hand, the head, the body and the feet. For none, or for a number that isn't a slot, it returns nowhere. A string view points at the text without copying it, and the names are literals in the program, so they're there for as long as it runs. In content dot c c, client hash is the fingerprint of what the clients read, so that a client can tell whether a world has its content. It took each item's id, name and look. Now it copies the whole item and clears its weapon, so it takes all of an item but its weapon, as the bag is going to show what an item adds, what it rolls and what using one does. A weapon's numbers stay the server's, so that changing a sword's damage still needs no new client: the clients show all of an item but how its weapon fights. In test content dot h, the tests' sword is in the kit, and worn in the main hand, as the checks want. In the content tests, str cat comes in, for putting items' text together. The test that reads the game's content takes the sword from the kit's first item now, as the kit is what the items file names now, and checks that it's held in the main hand. Ranges any of is true when its lambda returns true for any of what it's given. Something in the kit heals, which is the potions, something in the file stacks, which is the potions and the bones, and something rolls, which is the Ash Blade and the buckler. So the game's file has each of the new parts, and the kit has a sword for the hand, and potions to drink. The hash test gives the cap more armour, in a second copy of the content, and checks that the hash changes, as the bag shows what an item adds, so that's the clients' too. Clearing the weapon is what lets the damage change without a new hash, which the start of this test checks. Then the refusals, each next to its twin that's accepted, so that a test can't pass for the wrong reason, as a check that refused everything would. K sword is a sword in text, held in the main hand, which parses, and the refusals for the kit and the retired ids each add a single part to it. A sword held in the main hand parses, and the typo test from episode twenty-six stays. Two swords with one id don't parse, as str cat puts two copies of k sword together, which gives two items with the id one. A sword worn on the head is refused, with the message that says why, and so is a cap that's worn nowhere. The twin of the sword on the head is k sword, which parses, so what tells the two apart is the slot, and the message says which item it is. A bone that stacks to fifty parses, as bones are something to collect a pile of. A bone that stacks and rolls a modifier is refused, as a stack of bones couldn't keep a roll for each of them, and the message says it stacks, so it can't roll its own. A blade that rolls up to two more damage parses, as each blade takes a slot of its own, with its roll. And a blade that rolls from three to two doesn't, with a message that gives both ends, and the stat it rolls on. A roll from three to two would ask for a random number from an empty range, which C++ leaves undefined, so the check keeps that from ever reaching the code that rolls. A kit with the sword parses, while a kit with an item the file doesn't have is refused, and its message names the item that's missing. And a kit that starts a player with a stack of two swords is refused too, as a sword takes a slot of its own. Another retired id is fine, while an item can't have an id that's retired. Each expect equal checks the whole message, so that each refusal is refused for the reason its test names, and not for a typo somewhere else in the text.

17:57 After the build, sped up again, the content's tests read the game's items file. They refuse the items in their own text that don't add up, each next to its twin that's accepted, and all six pass. The world servers and the client read that file as they start, so they check the new items too, and the game plays as it did, with the sword from the kit.

A bag

18:18 A player's bag is going to be saved, and sent to their client, so it's going to be a message of our protocol, in nanommo dot proto. The protocol imports the items file's schema, as a bag holds stacks of items, and items, their slots and their stacks are the items file's. A place is where an item is: in a slot of the bag, counted from zero, or worn. A oneof holds one of its fields at a time, and says which, so a place is a bag slot or a worn slot, and not both. Setting one clears the other, and its where case tells us which is set, which a bag slot of zero needs, as zero is what a field left out reads as. An inventory is what a player carries and wears. The bag maps a slot's number to the stack in it, so an empty slot takes no room in the message, and what's worn is a map by slot too. The slot is a plain number there, as a map's key can't be an enum. And the revision is a number that goes up with each change. Whoever keeps a copy of a bag can tell it's changed by that number alone, without looking at its items, and a bag at zero hasn't been anyone's yet. The protocol's generated code includes the items file's now, so in the protocol's CMake file, the protocol links the content library. That makes CMake build the items' generated code first, and puts it on the protocol's include path. The bag's code goes in a new header, inventory dot h, with c std int for the counts, utility for std move, and abseil's status for what a move returns. Items dot h has the item table, and nanommo dot p b dot h the messages. Inventory is what a player carries, in a bag of k bag slots, and wears, an item in each slot. Its functions are what can happen to a bag, so that no code outside the class edits a bag's stacks by hand, where it could get a rule wrong. A bag has twenty-four slots, as a constant on the class. By default, a bag is empty, and one can be made from a message, as a saved bag is going to come back as one. It's explicit, so a message doesn't turn into an inventory without our asking. Items gives the message, to save or send, and revision gives its revision. At gives what's at a place, or a null pointer when it's empty, or isn't a place at all, such as a slot past the end of the bag. Add puts a stack into the bag: onto stacks of the item that have room first, then into empty slots, from the first. It returns how many didn't fit, as a full bag has to leave something behind. Add takes the item table, and so do wear and move, as the bag keeps no content of its own, so that it stays a plain value that can be copied around with a player. Wear wears an item, if it's worn and its slot is free, and returns whether it did, for a new player's kit. Move moves some of what's at one place to another, or all of it given zero, which is the default. It goes into an empty place, onto a stack of that item, as far as it has room, or it swaps with what's there. What ends up worn has to be worn in its slot, and otherwise, the error says why not, in words for the player, so that the server can pass it on. Take one takes one off a stack, as using one does, and destroy throws away all of what's at a place. In the private part, mutable at gives a place's stack to change, put puts a stack at a place, and clear empties one. Changed takes the bag to its next revision, and each change calls it. The bag keeps its message as its member, so saving it and sending it take no conversion. Items the table doesn't have, such as one taken out of the content, stay where they are, so that nothing is lost by a mistake in the file, but they can't be worn or stacked. And two functions make places: in bag makes a slot of the bag, and worn makes a slot that's worn. In inventory dot c c, algorithm comes in for std min, string for the messages, and utility for std move and std swap. Is place checks that a place is one. Where case says which of the oneof's fields is set, and the switch has a case for each, so the compiler warns us if the oneof gets another field one day. A bag slot has to be below k bag slots, and a worn slot has to be a slot, and not none. Slot is valid is protobuf's check that a number is one of the enum's values, as a client can send any number at all. Key gives a place's number in its map, which is the bag slot, or the worn slot as a number, as both maps go by number. The next function checks whether two places are one place, with one field of the oneof set in both, and one key. Find looks an item up in the table, and gives a null pointer for one the table doesn't have. Stack size of gives an item's stack size, and one for an item the table doesn't have, so that an unknown item doesn't stack. Check fits says whether a stack can go at a place. Anything goes in the bag, and a slot that's worn takes something that's worn, in that slot. So it refuses what isn't worn at all, saying you can't wear the bone, or that, for an item we don't know. And it refuses what's worn in another slot, naming the slot it goes in, with slot name from the items. That's the last of the helpers, which are in an unnamed namespace, like check parts, as nothing outside this file needs them. At returns a null pointer for a place that isn't one, so that to the code that calls it, a bad place from a client is an empty place. Otherwise it picks the map the place is in, with has bag, and finds the place's key in it, returning a pointer to the stack, or a null pointer when the key isn't there. Mutable at does that lookup on the mutable maps, which protobuf gives us through mutable bag and mutable worn, as a const function can't hand out something to change. A pointer into a protobuf map stays good until its entry is erased, so the callers use it straight away. Put picks the bag's map or the worn one, as mutable at does, and sets the place's key in it to the stack, replacing what's there, or making the entry, as a map's square brackets do. Clear picks the place's map with has bag too, the bag's or the worn one, and then it erases the place's entry, so that an empty slot takes no room in the message. Add starts with all of the stack left to put somewhere, which count of gives, reading a count that's left out as one. An item the table doesn't have goes nowhere, so all of it is left, and the bag doesn't change. Otherwise, it notes how many it had to put somewhere, and the item's stack size. An item that stacks goes onto the stacks of it in the bag first, slot by slot. The map's find looks each slot up, and a stack of that item with room takes as many as it has room for, or what's left, whichever is smaller, which std min gives. The stack's count goes up by that, and what's left goes down by it. Then what's left goes into empty slots, from the first, with a protobuf map's contains saying whether a slot has anything in it. Each takes up to the item's stack size, in a copy of the stack, so it keeps its rolls. If anything went in, the bag has changed, and add returns what's left. Wear finds what it's given in the table, and then it turns down an item the table doesn't have, one that isn't worn, and one whose slot already has something in it, returning false. Otherwise, one of it goes in its slot, with the stack's rolls, and that's a change too. Move is where the rules of a bag are, so it checks the most. From and to both have to be places, or it's an invalid argument, which a client that works doesn't send. And there has to be something at from, or the error is not found. A move to where it is does nothing, and isn't an error, as a drag that ends where it started is nothing to refuse. What moves has to fit where it goes. The if declares the status in its own condition, so the status is in scope for the if, and gone after it. Zero moves all of it, and so does a count of more than there is. Into an empty place, the part that moves goes, and what's left stays, or the slot it came from empties. We copy the part out before clearing where it came from, as clearing it erases the stack that moving points at. Onto a stack of that item, if it stacks, as many go as there's room for, and a full stack refuses. The room is worked out so that it can't go below zero, as a stack can hold more than its size if the content shrinks it later. Anything else is a swap, of whole stacks, so that part of a stack can't push another item out of its slot. And what comes back has to fit where the move came from, as swapping a worn cap with a bone would put the bone on the head. Std swap then swaps the two stacks in their maps. Whichever way it went, the move is a change. Take one finds the stack it's asked for, which has to be there, takes one off it, and empties its slot when it was the last, which is a change either way. Destroy asks at whether there's something at the place, then clears it, and that's a change too, so the revision goes up. And in bag makes a place, setting the oneof's field for the bag, and worn makes one for a slot that's worn, setting the other field, before each returns it.

Testing the bag

27:36 The tests need items of each kind, so in test content dot h, gear is what a player might carry: something that stacks, something that's worn, and something that rolls. Parse items reads them from text, as it does the game's items file, so they go through the checks too. Its kit comes first, the sword and three potions, like the game's. There's a sword, worn in the main hand, a potion that stacks to ten and heals, and a bone that stacks to fifty. A cap for the head adds two armour, and ten health, to whoever wears it, and a blade, which hits harder than the sword, adds one damage while it's worn, and each one rolls two to four more. Then the tests, in inventory underscore test dot c c, with c std def, c std int, map for counting items, random for the property test, utility for pairs, and vector, along with GoogleTest and the test content. Each of the gear's items gets a name for its id, so that the tests read by item. Of, in an unnamed namespace like the other helpers, makes a stack of an item, of one unless we say, and sets the count even for one, as the bag keeps its counts written out. Holds asks the bag for the stack at a place, and returns its item and count, or zero and zero when there's none, so that one expect equal checks both. Std pair without its template arguments works its types out from what it's given, which C++ seventeen added, and both ends of the condition give a pair of two unsigned numbers. And counts adds up how many of each item there are, wherever they are, in the bag or worn, by going through both maps. The square brackets in the loop give the slot and the stack of each entry, as structured bindings, which name the two halves of a map's pair. A map of item to count compares in one expect equal too, so the property test can check the whole bag at once. The first test is about adding, as a player who picks up a bone wants it on their pile of bones, and not in a slot of its own each time. So the test adds thirty bones, three potions and twenty-five more bones. The bones come in two lots, as a pile usually fills up a few at a time. The second lot fills the first stack to fifty, and the five left over go into the next empty slot, after the potions, which Holds checks slot by slot. The second is about a bag that's full, where whoever gives a player something needs to know what didn't fit, to tell the player what they left behind. It fills twenty-three slots with caps, which don't stack, and puts forty-five bones in the last, asserting on each add, so that a failure stops the test where the bag first went wrong. Eight more bones leave three behind, as the last slot has room for five, and another cap leaves one, as caps don't stack. An item the table doesn't have goes nowhere, and none of it fits. The third is about splitting and merging stacks, which is what a player does dragging part of a pile to another slot, and back. It moves ten of thirty bones into an empty slot, which splits the stack, and then all of them back, which merges them, and leaves the slot empty. Holds gives zero and zero for the empty slot, so the test checks that nothing was left behind in it. Then fifty more bones go onto the stack of thirty, which has room for twenty, and the other thirty start a stack of their own, in the next slot. Moving those onto the first stack is refused, with a message that says the stack is full. The fourth is about the worn slots, whose rules are what keep a cap off a player's hand. It puts a cap and some bones in a bag, and tries to wear the cap in the main hand, which is refused, with the slot it goes in. Each refusal in this test is checked by its message. A bone on the head is refused too, as a bone isn't worn at all, so no worn slot takes it. The cap on the head is fine, and the moves that work are checked by their status, with ok, and then by what holds says is in the slot. Worn, from the bag's header, makes the place of a slot that's worn, so the tests read like a player's moves. Then taking the cap off onto the bones is refused, with the message for the bone, as the swap would put the bones on the head. Taking it off into an empty slot is fine, and after a move to slot seven, holds finds the cap in slot seven. The fifth is about swapping, so that a player changes weapons in a single move, by dragging the new one onto their hand. It wears a sword, through wear, which then refuses the blade, as the hand's taken, and the bone, which isn't worn at all. Moving the blade from the bag to the hand swaps it with the sword, which ends up in the blade's slot. Wear leaves what's worn where it is, as it's for a new player's kit, and a new player has nothing on, so what it turns down is going to go in the bag. And four of ten bones can't swap places with a stack of potions, while all ten of them can, and the two stacks end up in each other's slots. The sixth is about places that aren't there. A client can send any place, and a modified one is going to try, so each bad place has to come back as an error, and not put an item in a twenty-fifth slot. The test refuses a slot past the end of the bag, and a place with neither field set. That's what a client sends when it leaves the place out, so it reads as no place at all, and not as slot zero. The test refuses a move from an empty slot too, as there's nothing there to move, and a move to the last slot, the twin of the one past the end, is fine. The seventh is for using and throwing away. It puts two potions and nine bones in a bag, and takes potions one at a time until the slot's empty, as drinking takes one off the stack. Holds checks the count after the first, and after the second the slot is empty, so the next take one fails, as an empty place is not found. Then destroy throws all nine bones away at once, in one call, as throwing a pile away takes all of it, and counts finds nothing left in the bag at all. A second destroy finds nothing there, and both of them report an empty place as not found, so that the server can tell the player there's nothing there. The eighth checks the revision, which whoever saves or sends a bag is going to compare. A revision that moved without a change would save a bag for nothing, and one that stayed put after a change would lose that change. A new bag starts at zero, as it hasn't had anything in it yet. After an add of five bones it's at one, and after a move of them to the next slot, it's at two, each checked with expect equal on revision, straight after the change. Then what changes nothing isn't a change, and leaves the revision where it was. The test tries three of those: a move from slot zero, which the move before emptied, so it's refused, a move to where it is, which goes nowhere, and an item the gear doesn't have, which add can't put anywhere. Through all three, the revision stays at two. The ninth is for an item that's gone from the content, which a player may still have saved. It starts from a bag as it would come back saved, at revision four, with an item ninety-nine, which the gear doesn't have. It can move around the bag, it can't be worn, with a message that calls it that, as the bag has no name for it, and it's still there after, at revision five. And the last is a property test, which checks what has to stay true whatever a player does, instead of what one move does. Players are going to drag their items around for as long as they play, so no run of moves can be allowed to make an item from nothing, or lose one. Like the other tests, it starts from the gear, which says what stacks and what's worn where. It makes its moves with a random generator from a seed, so that a failure comes back each time we run it. Any place is a lambda that gives a random place. It captures the generator by reference, so that all the lambdas draw from one sequence, which the seed decides. Bernoulli distribution gives true with the chance it's given, and uniform int distribution gives a whole number between its two ends, both of them included. One time in five it's a worn slot, from zero to six, which takes in none and a number past the last slot. Otherwise, it's a bag slot from zero to twenty-four, one past the end, so that places that aren't there come up too. Any stack picks a place to move from. It lists the places with stacks, in the bag and then worn, into a vector of places, going through the bag's map with a loop like the one in counts. Then it goes through the worn one, turning each worn slot's number back into a slot with a static cast, as that map keys them by number. When the vector is empty, or one time in ten, it gives any place instead, which it captures too, by reference, so it can fall back on it. Otherwise, it picks one of the places in the vector at random, with a uniform int distribution over its indices, from zero to the last. So most moves come from somewhere with something in it, and most moves move. Stacks counts the stacks in the bag and worn, so that the test can tell a split from a merge. Expected is how many of each item the bag should have, which the test keeps up to date as it adds and throws away. And it counts the splits and the merges, and whether anything was worn, so that it fails if the moves stop doing those. A property test whose moves all failed would pass, and test nothing. Then come twenty thousand steps, each of which moves something, with an add or a throw-away now and then. Each twentieth step, something new comes in, with an item's id from one to five, picked at random. Then it works out the most it adds, which is sixty of what stacks, and one of what doesn't, as sixty of those would take a slot each, and fill the bag. Then it picks a count at random, from one up to that most. Add puts the stack in and says how many it left over, which can be all of it when the bag is full. Assert less or equal checks that it doesn't report more left over than it was given, and expected goes up by what fitted, if anything did. Each fifteenth step picks a place with any stack, and if there's a stack at it, takes the stack's count off what the test expects for its item. Then it throws the stack away, so that the bag keeps changing, and doesn't stay full. Before each move, it notes the revision and the number of stacks, to compare with after. Then, whatever else it did, each step moves a random count, from zero, which moves all of it, to twenty, from a random stack to a random place. The revision can't go down, and a move that fails leaves it alone. More stacks after a move is a split, and fewer is a merge. And then the property: the items, counted by item, are what we expect, with the step in the message when they aren't. Assert, where expect would carry on, stops the test at the first failure, as the steps after it would be checking a bag that's already wrong. Each stack in the bag is in a slot that's there, holds at least one, and no more than its stack size, which add and move each have to keep to. And each item worn is one item, worn in its own slot, which the gear's equip says. At the end, there have to have been over a hundred splits, over fifty merges, and something worn. A first version of this test filled the bag with things that don't stack, so it made almost no merges, and a bug in merging passed it, which is why it counts them now. And in the server's CMake file, inventory dot c c goes into the server's core library, and its test into the server's tests.

38:57 Docker builds the game at this step and its tests, sped up here, as a build from nothing takes a couple of minutes. Then it runs the ten tests of the bag, from adding onto stacks to keeping the items the content doesn't have, and they pass. The last of them, the property test, makes its twenty thousand moves in fifteen milliseconds. Nobody carries a bag yet, so the game plays as it did, with each player holding the kit's sword.

Wrap-up

39:22 So, today the world server keeps one copy of the content, which each copy of each land shares. An item is the parts it has: how many stack, where it's worn and what it adds, what using one does and what each one rolls, and the items file has seven of them. The content's checks refuse parts that don't go together, each test next to its twin that's accepted. The server has a bag, of twenty-four slots and five worn ones, which stacks, splits, swaps and wears items, and a property test moves items around it twenty thousand times, and checks that none is made or lost.

Playing it

40:45 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.