nanoMMO

MMORPG loot tables and drops on a C++ server

Monsters drop loot from tables in the content, each item with its chance, into the killer's bag, the chat says what dropped, and the bots drink and dress.

Watch on YouTube

We give our MMORPG's monsters loot tables. Each kind of monster lists the items it can drop, each with a chance and how many, and when it dies, the server rolls them and puts what drops into its killer's bag, a stack at a time or one at a time with rolls of their own, and the chat says what dropped, or what didn't fit. The content's checks refuse loot that doesn't add up, the bots drink potions and wear what they find, and the world fuzzer moves items to any place a client could send. This is episode 42 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 ep042/end.

  1. 2:11ep042/step-1Loot tablesBrowseChanges
  2. 13:51ep042/step-2Bots that drink and dressBrowseChanges

Transcript

Intro

0:00 Hello and welcome back to episode forty-two, where we're going to have the monsters drop loot, from tables in the content, into the bag of the player who killed them. Let's begin with a quick look at where we're going to be by the end of the episode.

0:14 With alice's bag open, at level four, let's fight the skeletons in the Meadows that come for her. Each kind of monster has loot now, the items it can drop, each with its chance. When one dies, what it drops goes straight into her bag, and the chat says what it dropped.

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. The server sends each player their bag over TCP, as they join and as it changes. It takes what they do with an item, a move, a use or a throw, with a rate limit, and a refusal in words in the chat. The client draws the bag in a window of its own, each item's icon in its tint, with a tooltip that says what it is. The character window has a row for what's worn, and a drag moves an item, stacks it, wears it or takes it off, with the server deciding.

Today

1:19 Each player has a bag on screen now, but nothing puts anything in it past the kit they start with. So each kind of monster is going to have a loot table in the content, the items it can drop, each with its chance and how many. The content's checks are going to refuse loot that doesn't add up, like a chance of more than one, or an item the items file doesn't have. When a monster dies, the world rolls its loot, and puts what drops into its killer's bag, with rolls of its own for a blade, say, so that two blades can have different damage. And the chat says what it dropped, or what didn't fit. Then the bots are going to drink potions when they're hurt, and wear what they find. That way, under a load test, the servers save and send bags as they would for players, and the world fuzzer is going to play with the bags too. All right, let's get started, with the content.

Loot tables

2:12 What a monster drops goes in the content, next to the rest of what it is, so in monsters dot proto, a message of its own, drop. It has the item, by its id in the items file, and the chance of it, a float from nothing to one. Then how many, from min to max, and over them, we write that without them, it's one. Over the message, we write what it is: what a kind of monster can drop, which goes into its killer's bag, the item, the chance, and how many. A kind gets its loot, as a repeated field, a list of drops, which can be empty, for a kind of monster that drops nothing. In monsters dot t x t p b, the skeletons get their loot. A skeleton of the Meadows drops one to three bones, more often than not, at a chance of point six. Then a potion, one time in five, and a leather cap or a leather tunic, each one time in ten. The Ash Skeleton, in the Ashlands, drops bones too, and potions more often, at point three. And it drops the Ash Blade and the Ash Buckler, named for its land, each one time in ten. The Crypt Skeleton drops more of all of it: two to four bones, at point eight, one or two potions, half the time, and the blade and the buckler, each one time in four. A dungeon is where the better loot goes, as it's harder to get to. In monsters dot h, parse monsters refuses loot too: a drop with a chance of nothing or less, or more than one, or from more to fewer. In monsters dot c c, after the kinds are parsed, each kind's drops are checked. A chance of zero or less can't drop, and more than one can't be a chance, so either is an error that names the kind, the item and the chance. A chance that's left out is zero, so this catches a drop that forgot its chance too. And a max that's set, and less than the min, is an error too, from more to fewer of an item. A max of zero is a max that's left out, which the world takes as the min, or one. A drop names an item, which is in another file, so the monsters' file can't check it alone. In content dot h, check content checks what the whole folder can tell is wrong, so it gets the drops. We write that over it: a monster that drops an item the items file doesn't have, or a trainer that teaches a skill the skills file doesn't have. In content dot c c, read content's error named the NPCs' file for whatever check content found, as the trainers were all it checked. It can be about the monsters now, so the error names the folder, with check content's message. Check content goes through each kind of monster, and each of its drops, and an item that the items file doesn't have is an error, with the kind's name and the item's id. In content test dot c c, the test refuses loot that doesn't add up. A skeleton that drops one to three bones, at a chance of a half, is accepted, so the refusals that follow are each about their own mistake. A drop with no chance is refused, and the test compares the status's message in full. It says the skeleton drops item three with a chance of zero, which has to be more than zero and at most one. A chance of one and a half is refused too, as more than one isn't a chance. And so is a drop from three to one, with a message that says the skeleton drops from three to one of item three. Then the game's own content, which has to read, and over it, we write that what a monster drops has to be in the items file. Some kind in it has loot, which any of checks with a lambda, so the test knows the file it reads has drops. Then we add a drop of item ninety-nine to the skeleton, and check content says that the skeleton drops an item ninety-nine that isn't in the items file.

5:49 Docker builds the game at this step, sped up here, and the content's tests read the game's files, so they need nothing else, and run with dash dash no deps. They all pass, the new one with the loot's refusals too, and the game's content still reads, with the skeletons' loot.

Rolling the loot

6:07 Now the world can roll the loot when a monster dies, and it's going to have something to tell the killer: what dropped. The world doesn't send anything, the server does, so in world dot h, vector comes in, for a list of lines that the world keeps for the server. A notice is such a line, and over it, we write what it is: a line the world has for one of its players, such as what a monster they killed dropped, for the server to send them. It has the player's id and the text. In the class, take notices hands over the lines for players since the last time, and we write that over it. The world hands them over, so each line goes once. In the private part, loot puts what the monster the player's just killed drops into their bag, and the world keeps its notices in a vector, next to its random generator. In world dot c c, c standard def comes in, for size t, random, for the distributions that roll the loot, string view, for the vowels, and vector. In the unnamed namespace, some writes how many of an item, for the chat, and we write what that looks like over it: a bone, an ash blade, or three bones. More than one is the count, the name and an s. One is a or an, and an when the name starts with a vowel, which string view's find looks for in the five of them, a e i o u. List joins things for a line of chat, and over it, we write what it makes: a, a and b, or a, b and c. It goes through them, and appends each one after nothing for the first, and after the word and for the last, and after a comma for the rest, with two conditional operators, one inside the other, for the three cases. In hurt, once the monster is dead, and the experience has gone to its killer, loot rolls its loot. Loot finds the monster's kind in the content, and a kind that isn't there drops nothing. Kept is what went into the bag, and left is what didn't fit, as lines for the chat. For each drop in the kind's loot, the item has to be in the content, which check content makes sure of, and a Bernoulli distribution says whether it drops. It's true with the drop's chance, and false otherwise, so a chance of point six drops six times in ten. It's rolled with the world's random generator, like the monsters' wander, so the fuzzer, whose world comes from a seed, plays the loot back the way it went when it finds a problem. Least is the drop's min, or one without it, and a uniform int distribution picks the count, from least to the max, or least when there's no max. Then over each, we write how the items drop: what stacks drops as one stack, and what doesn't one at a time, each with rolls of its own. So each is the count for an item that stacks, and one for an item that doesn't. The loop makes a stack each time round, of each of the item, which is once for the bones and once per blade. For each of the item's rolls, the stack gets a modifier of that stat, with an amount that a uniform int distribution picks from the roll's min to its max. So an Ash Blade can roll from nothing to five more damage, and the tooltip tells two of them apart. The bag adds the stack, onto stacks that have room and then into empty slots, and returns how many didn't fit, so fitted counts the ones that did. After the loop, what fitted goes on kept, as some of the item, and what didn't goes on left. When something went into the bag, the player gets a notice: the monster's name, dropped, and the list of what it was. And when something didn't fit, another notice says that their bag is full, so they left it behind. There's no ground to drop it on, so what didn't fit is gone. Take notices returns the notices with standard exchange, which leaves an empty vector in their place, so the next call has the lines since this one. Each copy of a land has a world, so in copies dot h, copies gets take notices too, and we write that it's the lines each copy has for its players since the last time. In copies dot c c, algorithm comes in, for ranges move. Take notices goes through the copies, and moves each world's notices onto the end of one vector, with a back inserter, which pushes back what's moved into it. In server dot c c, the tick steps the copies, then takes their notices. Each player gets theirs with notify, as a line from nobody, like what a trainer says. Notify sends on the player's session, so the tick checks that the player has one first. In test content dot h, with takes the monsters too, after the skills, empty by default, so that a test's world can have monsters with loot. And we write that it's content with these items, skills and monsters, and nothing else, for a world.

Testing the drops

10:38 Then the world's tests, in world test dot c c, where vector comes in, for the notices. Looted is a table of monsters with one kind, the tests' skeleton, and over it, we write what's different: a skeleton that always drops two bones and a blade, and whose health one swing takes. Its most health is ten, which the sword's fifteen takes in one blow. It drops bones at a chance of one, with a min and a max of two, so it's two of them each time. And a blade, at a chance of one, with no min or max, so it's one. With a chance of one, the test doesn't depend on how the rolls go. Kill kills the looted skeleton in front of the player. It takes the looted kind, spawns one a metre away, has the player attack it for a second, which is enough for a blow, and stops the attack. The first test puts what a monster drops into its killer's bag. The world's content is the gear, with the looted skeleton, and a new player joins with the kit, then kills one. The kit's potions are in the first slot, so the loot's in the next two. Slot one has the bones, two of them, and slot two has the blade, which is item five, the tests' blade. Its stack has to be there before the test reads it, so that a missing blade fails the test with a message, and doesn't crash it. It has one roll, as the tests' blade has one range to roll, and we write that the blade rolled its damage, from two to four, which the checks after it look at. Then the notices, where there's one, for player one, which says that the skeleton dropped two bones and a blade. And taking the notices again gives none, as they're handed over once. The second test leaves what doesn't fit in the bag. Over the player, we write what she carries: a sword in hand, and a cap in each slot of the bag. With makes her a player with the sword worn, and a loop over k bag slots adds a cap for each. Caps don't stack, so twenty-four of them fill all twenty-four slots, and her bag has a revision, so she doesn't get the kit. She kills a looted skeleton, and her bag still holds twenty-four stacks. The notice says that her bag is full, so she left two bones and a blade behind.

12:51 Docker has the game and its tests at this step already, from the content's tests, and the world's tests run without MySQL, Redis or Consul. They pass, the earlier ones too, whose content has no monsters, so their kills drop nothing. The last two are the new ones, the loot in the killer's bag and the bag that's full.

13:11 So let's log in as alice, at level four, and open her bag, which has the kit's three potions in its first slot. She stands next to the home of one of the Meadows' skeletons. Skeletons come for her, and she fights them with the sword and with Thrust, and when one dies, its loot is rolled. What drops goes straight into her bag: a potion would go onto the kit's three, and the rest into the slots after them. The chat on the left has a line for each kill that dropped something, from the world's notices, each a line from nobody, and the new items are in her bag, each icon in its tint. The first one's tooltip says what it is: a bone, which stacks up to fifty.

Bots that drink and dress

13:51 The bots get loot too, now, and do nothing with it. A load test is meant to make the servers do what players make them do, and players drink potions and wear what they find, which the server saves and sends. So the bots are going to do it too. In the bots' folder, a header of its own, bag dot h, which includes optional, the content's items and the protocol. Tend is what a bot does with its bag, if anything, and it takes the items, the bag, the bot's health and the most it can have, and whether it can drink again. Over it, we write what it does: it drinks something that heals, once it's down to half its health and can drink again, or else wears something that's worn in a slot it has free. It's a function of its own, apart from the bot's connection, so that a test can call it with a bag. In bag dot c c, c standard int, optional and the protocol's places come in, for in bag and worn, which tend's moves are made of. Tend's arguments are the header's, and its body is a loop over the bag. Tend works out whether the bot is hurt: alive, with less than half the most health it can have. Twice the health, under the most, is that test in whole numbers. Then it goes through the slots of the bag, and skips one that's empty, or whose item the bot doesn't know, with an iterator to the item, or the table's end. A hurt bot that can drink uses the first item that heals, a potion, from that slot. Otherwise, a bot that isn't hurt wears an item that's worn, when the slot it's worn in has nothing in it: a move from the bag's slot to the worn one. With nothing to do, tend returns standard null opt. In the bots' CMake lists, the bots' library builds bag dot c c, and the bots' tests get a file for it, with the content's folder as a definition, like the client's tests. In bot dot h, chrono comes in, for when the bot last drank, and the content's header takes the place of the NPCs'. The bot takes the content, in place of the NPCs' table, and we write that it finds the NPCs that talk through their kinds in the content, and what its items do through the items. It keeps a reference to the content, and the bot gets its health, the most it can have, and its bag. And when it last drank something, which it does once its cooldown is over, as we write over it. In bot dot c c, bag dot h comes in. K drink every is ten seconds, a potion's cooldown, and we write why: so that a bot doesn't ask for one the server refuses. The constructor keeps the content, in place of the NPCs. When the bot's bag comes, it keeps it, where it threw it away before. Among the world's players, the bot finds its own, and keeps its health. The most it can have comes once a second, with the names, so it keeps that when the world has it, and we write that over it. The NPCs come from the content's NPCs now. Then wander tends the bag, after the skills, and over it, we write why: as a player would, a bot drinks a potion when it's hurt, and wears what it finds, which keeps the servers saving and sending bags. Tend gets the bot's bag and health, and whether ten seconds have gone by since it last drank. When tend asks for something, it goes in the input, and if it's a drink, the bot keeps the time, for the cooldown. And in main dot c c, the swarm of bots takes the content, in place of the NPCs, and hands it to each bot it makes.

17:20 The bots' tests, in bag test dot c c, include c standard int and optional, the content's header, to read the game's items, and GoogleTest. The potion is item two and the cap is four, and items reads the game's items, from the content's folder. Of makes a stack of an item, one by default. The first test drinks once it's down to half its health. A bag with two potions in slot three, and at sixty health of a hundred, tend does nothing. At forty, it asks to use slot three, and the test asserts that there's an action, and that it's a use, before it reads the slot. Over the last two, we write when it doesn't: not while the last one's cooling down, nor once it's dead, at zero health. The second test wears what's worn in a slot it has free. With a cap in slot five, at full health, tend asks to move it. The move has to be there, and it's from the bag's slot five, onto the worn slot of the head, where a cap goes. Then, with a cap on the bot's head already, tend does nothing with the one in its bag. Then the world fuzzer, in fuzz slash world dot c c, which plays the world with any input a client could send, and checks what has to stay true. The bags are new, and a client can send any place, so the fuzzer is going to move items to and from any of them. The content moves out of the lambda that makes the island, into a static of its own, as the checks at the end read the items too. Any place is a place that the fuzzer's input picks, with a bool for the bag or worn. In the bag, it's a slot from zero to thirty, where the bag has twenty-four, and worn, a slot from zero to seven, where there are five, past slot none at zero. Over it, we write which places those are: a place in the bag or worn, past the ends and in slots that aren't slots too, as a client could send. The switch has three cases more, from five to seven. Five moves from any place to any place, with a count from zero, which moves the whole stack, to sixty, past a stack of fifty bones. It ignores the error, as a refusal is fine. Six uses a slot from zero to thirty, ignoring a refusal too, and seven throws away what's at any place. The checks at the end are what the fuzzer is for. At the end, the checks, and over them, we write what they are. No stack holds more than it can, nor nothing, in a slot that's there, and what's worn is worn in its slot. So each stack in the bag is in a slot under twenty-four, with at least one, and no more than its item stacks to. And each item that's worn is a single one, and the slot its item is worn in, from its equip part, is the slot it's in, with the map's key cast to a slot. Abseil's check stops the fuzzer, with the values, when any of them is false.

20:06 Docker builds the game and its tests at this step, sped up here, as a build from nothing takes a couple of minutes. The bots' tests are a binary of their own, and they need nothing else, so they run with dash dash no deps, with a filter for the bag's two. Both pass, the drink and the cap on its head. Then Docker builds the fuzzers, from the stage that builds them with clang's fuzzer and sanitisers, sped up here too. And the world fuzzer runs for a minute, with dash seed one, so that libFuzzer makes its inputs in one fixed order. They have the moves, uses and throws on the bags now, with the checks after each input. It's done after its minute, with nothing found.

20:49 With forty bots playing on Ember in Compose for two minutes, let's ask MySQL what they carry, in their bags and in their hands, each item by its id with how many there are between them. Item one is the kits' sword, forty of them, and two the potions, a hundred and twenty in forty kits, less what the bots drank. Then what the skeletons dropped, which isn't much in two minutes, as the bots wander more than they fight. Then what's worn, by the slot it's worn in. Forty swords are in the main hand, and the cap that dropped is on the head of the bot that found it, which put it on, and the saver wrote that like a player's.

Wrap-up

21:27 So, today each kind of monster has a loot table in the content, the items it can drop, each with its chance and how many. The content's checks refuse a chance of nothing or less, or more than one, a drop from more to fewer, and an item the items file doesn't have. When a monster dies, the world rolls its loot and puts what drops into its killer's bag, a stack at a time, or one at a time with rolls of its own. The chat says what dropped, and what didn't fit. The bots drink potions when they're hurt and wear what they find, so a load test has the servers saving and sending bags. And the world fuzzer moves items to and from any place a client could send, and checks the bags after each input.

Playing it

22:58 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.