Command Processing

2025-02-16

Basic Game Loop

When a player types a command in Agony Forge, their browser sends the command to the server over Web Sockets. The command is received by the WebSocketController as a String. The lowest level pattern in the core of the game engine is that every input is sent to a Question, and from there it is processed into a Response. Any Output generated by the Question is sent back to the user, and the current Question appends that output with a prompt. That's it. That's the entire loop.

It's simple, but powerful: a Question can do anything it wants with that input, from the normal in-game prompt to the character menu, to in-game editors or whatever other kind of interactive command line it needs. Part of the Response object has the output and the other part indicates which Question object should be used next.

In a menu with multiple screens, each screen is a separate Question and moving between screens is as simple as passing the next screen's name in the Response. Many Question objects are themselves simple state machines that model a screen in a menu, with a state for handling each of the choices in the menu.

To move between menus or in and out of an editor, the Response object can tell WebSocketController to swap to a different Question to prompt the user and interpret the next input.

In-Game Commands

In-game commands are handled by the CommandQuestion which has a secondary layer of processing for user input. It has a set of CommandReference objects that represent the names and priorities of commands the user could execute. The CommandReference refers to a Command which contains the actual logic for running the command. Most commands have a single CommandReference that points to a Command, but it doesn't have to be one-to-one. For example, I have a CommandReference for each directional command (north, east, south, west, etc.) that all refer to the same MoveCommand, just with different arguments to indicate which direction to move in.

CommandQuestion tries to match the first word of the input to a command, then sends the rest of the input as the command's arguments. The input is "tokenized", or split up into a list of words. The commands are sorted both by priority and alphabetically. That's why if you type "s" you get SOUTH instead of SAY. All the words after the first are sent as arguments to the command. Different commands expect different kinds of arguments, so there are a few typical ways that we process them.

Object and Creature Targets

Most often, commands have an argument that is an object in the game world like GET SHIELD where "SHIELD" is the name of an item on the ground. The command simply assumes the second token in the input will be the name of an item in the same room as the player, and it tries to find something that matches. If it can't, you get an error message like "You don't see anything like that here." If it does find the shield, the command will move it from the room to your inventory. I call this "object binding", because you're taking a word and trying to "bind" it to some kind of actual object (or creature) in the game world.

There are many varieties of object binding because commands need to match all kinds of different targets. An argument could be an item, a creature, or a player. It could be in the player's inventory or in the same room. It could be something in a different room. It could even need to be a specific kind of object like a container or a weapon. That's why it was easiest to write the logic for it individually in each command rather than creating a standard system for it. The code in a lot of the commands is similar, but not quite the same.

Quoted Strings

Other times the argument is a string, such as for communication commands like SAY or GOSSIP. The communication commands are actually a special case for this because we can short circuit the process and assume everything after the command should be taken literally as the message the player wants to communicate. For TELL there's a target and then the string, but the idea is the same. We don't want to process or otherwise change the user's input at all or we'd be changing what they wanted to say. So the command simply takes the command and arguments off the front and treats the rest as the message, keeping it exactly as the user typed it. I call this "implicit quoting" because the player doesn't have to type the quotes for it to work.

There are other cases where a command might have an explicitly quoted string in it, such as the ROLL command that takes an Effort (e.g. "Weapons & Tools") as an argument. You have to quote for it to work: ROLL DEX "Weapons & Tools". If you don't quote it, it splits the name of the Effort into multiple words and the command fails. When we see quoted strings, we treat everything between the quotes as a single "word" instead of splitting on the spaces.

Commands that require quoted strings are a good argument for aliases, a feature Agony Forge still lacks. In the old CircleMUDs I used to play, to cast spells you'd have to type out the name of the spell. In combat that could be quite time consuming and error prone, so you'd create an alias for the command: mm might map to CAST 'magic missile', and then you'd just have to type mm in combat. In Agony Forge, aliases will be a step at the very beginning of the CommandQuestion. If your input matches an alias, it will simply swap the input for the value of the alias before continuing. The rest of the command interpreter never even needs to know the input has been transformed.

ID Numbers

Some other commands, in particular commands for admins, take numbers as arguments. For example, IEDIT can take the ID number of any item that you'd like to edit. This edits the item template, and not a specific instance of the item. GOTO takes a room ID to transport you instantly from where you are to the room you specify.

Future Improvements

While it's good enough for the time being and I've moved on to work on other parts of the game, there are some key improvements that can be made to this system.

Small Words

One easy improvement would be to throw out tokens such as "A", "AN", "AT", "FROM", "MY", and other small words that the software doesn't need in order to understand the command. Then all of the following commands would become identical to one another, and interacting with the game would feel more like "natural language":

  • GET APPLE BAG
  • GET APPLE FROM BAG
  • GET APPLE FROM MY BAG
  • GET THE APPLE FROM BAG
  • GET THE APPLE FROM THE BAG
  • GET AN APPLE FROM THE BAG

Selectors

Imagine you are standing in a room with 50 swords in it. All of them have the "sword" keyword on them, and while some of them have other keywords, the one you want has the same keywords as a dozen of the other swords. Right now the GET command would get the sword it found first. To get the one you actually wanted, you could pick all of them up and then drop them one by one until you got to the one you wanted. Then stash that one somewhere else and drop all the rest. But that's a lot of work.

CircleMUD got around this with a strange "dot" syntax. If you want the 8th sword on the list, type: get 8.sword. It's not intuitive, but it did work.

I would love to try a more natural way with something I call "selectors". In regular English you might say something like: get the eighth sword. Assuming we're filtering out "small words" from the previous section, get eighth sword should work. The command just has to recognize that "eighth" is an optional argument related to the argument after it and that it modifies the search for "sword" to return the 8th result instead of the first. I haven't implemented this yet because it's pretty obscenely complicated to do with the way the code is currently structured, and would have to be duplicated into a dozen different commands.

Object Binding

A significant refactor would be to add another pre-processing step to match tokens to objects before the command ever receives the input. The command could define a syntax, declaring through annotations or method arguments which objects (not Strings) it requires. Our GET example above could declare that it needs first an inventory item inside a container, and next an inventory item that is a container. When the command is invoked, the engine could pass in the actual items already having been looked up. If no such items can be found, an error message can be generated before the command is ever invoked at all. The benefits of this are substantial, because it would standardize the code for looking things up into one place, and the error output for every command would be the same. Overall I think doing this would easily eliminate half of the code for commands as well as a huge number of repetitive unit tests.

Verb Binding

Another good improvement would be to always match the first token to a valid command name. The inputs "S", "SO", "SOU", "SOUT", and "SOUTH" are all going to match to the same command, so the token should always be normalized to "SOUTH". Since there is no binding to commands in the current system, each of those abbreviations of the command name would be different tokens.

References

These ideas are not entirely my own. Parsing is an area of computer science that has been around since the beginning, and there is a great deal of research and learning to lean upon when designing a system that needs to process user input. However there is one obscure source that has influenced me on this subject for a good long time now. Back in 1998 I read a post by Dr. Richard Bartle, the creator of MUD1 and MUD2, that described an improved not-quite-natural-language system for parsing commands on a MUD. I've built a system that was similar for an earlier MUD but it's an easy system for me to get bogged down in. For Agony Forge I have decided to build a good-enough command parser for now and I plan to circle back to it some time down the road after the game is playable.

The way Agony Forge works today is also inspired in some ways by how CircleMUD and Smaug MUDs do it, although more object oriented. In Circle based MUDs there is a list of commands that map to C functions. The list is sorted in priority and alphabetical order, the same as Agony Forge, and the first word in the input is matched to the command name that is the first match. Then the function is invoked with the rest of the arguments in a string. Each function is responsible for processing the arguments and mapping them to objects before doing the actual work of the command. It suffers from many of the same problems I described: error messages are inconsistent, a lot of code is duplicated, and each command has a blend of parsing and game logic.

Go to Comments

Wear Slot Modes

2025-01-04

In most MUDs a piece of equipment can take up one slot on your character. You wear a hat, it takes the "head" slot. You wear earrings and they take your "ears" slot. In some MUDs equipment can take up multiple slots. But what happens when you start to talk about large or bulky items and small or versatile items at the same time? Consider rings, and consider polearms. How do those work in this kind of system?

Many MUDs allow you to wear two rings at a time: presumably one on the ring finger of each hand. Your character has two "finger" slots, and each ring takes one of them. Some MUDs have a special "shield" slot that can only be used with shields, or special "dual wield" slots for weapons that take both hands. A lot of the time there is a bunch of conditional logic to deal with all of these different cases.

Here's what I'm doing with Agony Forge. There are actually two modes for wearing equipment:

  • All items can have one or more wear slots set to true or false.
  • All items can be set to either "ALL" or "SINGLE" mode.

Right now, characters all have two fingers. This will change in the future because I want wear slots to be defined by species and I want them to be able to be damaged or altered after character creation, but for the moment everybody has the same ones and that includes two fingers.

To create a ring, you'd set both finger slots on it, and set the wear mode to "SINGLE". When you do that, your ring would find the first "finger" that was available and occupy only that one slot. Your next ring would do the same, occupying the other slot, so you could wear two copies of the same ring or two different rings. You can equip and remove them separately because they're two different pieces of equipment.

Characters also all have two hands. This will also change in the future for the same reasons as above, but for now that's how the game works.

You can't create a polearm the same way as rings: it would only take one hand slot, and then you could have a second polearm in the second hand. While that would be awesome, it's probably just a little bit OP. So what you do is set it to use both hand slots, then change the wear mode to "ALL". When you wear the polearm now, it will occupy both hand slots, and it won't let you wear it if you're already holding anything in either of your hands.

This all works because the item has its possible wear slots defined on its ItemComponent (See the "Component Systems for Games" article for an explanation of what that is) but the LocationComponent defines the wear slots that it is actually occupying right now. When you wear an item in SINGLE mode it only sets one of the slots that are available both on the character and the item. However, when you wear an item in ALL mode it copies all of the slots as long as all of them are available to use.

I still have some things I'd love to add to this system, such as items that overlap other items. For example, you wear a shirt, then you put a jacket on over it. They occupy the same wear slot but the jacket prevents you from removing the shirt, and you can't wear two jackets but you could put on plate mail over the jacket? There are some complexities that I haven't fully worked out yet. It will probably be something like a "wear layer" where you can overlap things with a higher layer but not the same or lower.

Another possible future addition to the system is a size rating. Armor for halflings, for humans and for giants would more or less be shaped the same ways since all of those are humanoids, but their sizes are wildly different. A halfling could probably never wear a giant's armor without magic or so much alteration as to effectively make a new item. I think this could be a simple size rating on the item. Just a "small/medium/large" kind of thing would do. Maybe you could wear something in your size with no modifiers, and something one size up or down with a penalty, and farther away than that would be impossible. It would be really cool if there was magic that lets you break this rule, and tailors that could resize a garment into an equivalent one of a different size.

By supporting both SINGLE and ALL wear modes, Agony Forge has a very flexible model for equipment that should allow building almost any kind of item. It's also very dynamic, so that when we have characters with all kinds of different wear slots - more than two arms, no legs, several heads, tentacles, or whatever it is - the system still works without any special cases. Avoiding special cases is always one of my main goals in designing a system because it means the system will be resilient to change, and new things will work in sensible ways.

Go to Comments

Component Systems for Games

2025-01-03

I just got through a major redesign of Agony Forge's classes for characters and items, in preparation for adding Non-Player Characters (NPCs). Rooms are a bit of a different animal so they didn't get a redesign. For items and characters there was something called a "prototype" that I'm now calling a "template", which rooms don't have. The basic idea is that you build something, for example a sword. You want to stamp out a bunch of copies of that sword in the game. You don't want to individually build every sword in the game by hand, obviously. Thus, you edit the template in the editor, not the actual item.

I haven't implemented area based permissions yet but what I'm planning is that your area has a limited range of IDs assigned to it. In old Circle/Smaug based MUDs you'd get an area number with 100 IDs in it. For example, if your area was number 30 you could create any room, item or creature between 3000 and 3099. Area 31 would have 3100 to 3199. 100 numbers was often not large enough to fully realize the idea for an area so I'm hoping to come up with something more flexible than that, but that's the general concept I'm starting with.

We have two kinds of items and characters: a template and instances. Templates have a buildInstance() method that knows how to copy their properties into a new instance. Whenever you bring an item or a character into the game, you've created an instance. It has a reference back to its template so we know where it came from, but it also has an automatically assigned ID number in the database. Characters work the same way: when you make a new character, you're actually making a template, even though that fact is hidden from you. When you log into the game, it creates an instance of the template for you. When you close your browser the instance gets deleted but the template stays.

When I add NPCs I will have multiple kinds of characters that need slightly different information. I can handle that with components. Players and NPCs have mostly the same data but a few different things. Players have session IDs, some websocket stuff and usernames, while NPCs will need behaviors and other state to help determine their actions. Items and characters also share some of the same information, like location information about where they are in the world. Using inheritance to represent all of this resulted in a lot of duplicated code, which is why I landed on a component system. It's something that is commonly used in games of all kinds. If you've built anything in Unity, Unreal or even Roblox you've already seen component systems at work.

In Agony Forge items and characters all inherit from AbstractMudObject, which is a simple container for four different kinds of components:

  1. Player - information about a human player's connection to the game.
  2. Character - information about a character, like stats, species and profession.
  3. Item - information about an item, like where it can be worn.
  4. Location - information about where something exists in the game world, like what room it's in or who is holding it.

Every item and character has a Location. Anything without one doesn't exist anywhere in the game world. Items have an Item component, player characters (PCs) have both a Player and a Character. NPCs will have a Character and a fifth component I haven't created yet to hold information about their behavior. That's the beauty of a component system: it's modular and expandable. I can create new kinds of components without disturbing the existing systems, and I can reuse components like Location in several places.

In Unity everything is a GameObject with other components attached to it. I debated doing the same with Agony Forge. Reducing everything to a MudObject was possible, but I decided to keep it as MudCharacter and MudItem for one reason: it lets me search the database for characters and items separately without doing any filtering, because they're on separate database tables. It means I can have the same IDs for their templates: Item 1010 and NPC 1010 can both exist without ID collisions, which is important for how area permissions work.

So far the only downside of the component system has been that it makes unit testing complicated. Every object is now built out of several different objects, and they all have to be mocked two or three layers deep. I think I can ease some of that pain by building factory methods to construct fully formed mocks, but I do have to admit it is more complex to test than the old system. If I can find an interesting, elegant way to solve that problem I'll talk about it in a later article.

There you have it! When you're in IEDIT in Agony Forge, you're editing an Item template. When you do CREATE to make an item, you're telling the game to create an instance of your template by copying all of its values into the new item. When you get rid of it with PURGE you're deleting the instance but the template is still there. This structure lets us share code, like Location, between different kinds of objects. It lets us easily create different subtypes of objects without complex inheritance or duplicating code, such as Player Characters and Non-Player Characters, just by giving them different components.

Go to Comments

Access Logging in Spring Boot

2024-12-22

I've been working on getting a copy of Agony Forge up and running on a machine at my house, doing port forwarding from my router. I'm pretty good at this kind of stuff but I was having trouble with getting the websocket connection returning 403s. I thought to myself, "You know, it would be handy to have the access logs from Tomcat so I could see if the connection was getting all the way through or not. I'll just turn those on."

I found an article online that said you just need to add the following to application.yaml:

server:
  tomcat:
    accesslog:
      enabled: true

Great! That was easy! But... where are my logs? I don't see them.

It turns out they are written to a file with a randomly assigned filename in /tmp inside the docker container. I was able to get into the container and find them when I ran it on my laptop, but doing that on the other machine is complicated for various reasons. Popping inside that container is not going to be a feasible thing to do on a regular basis. I really needed them to just be there with all my other logs. Apparently, this is not a straightforward thing to do in Spring Boot 3.

I spent many hours fiddling around with something called a LogbackValve, which attaches to Tomcat and pipes the logs out. It reads a configuration file named conf/logback-access.xml by default, although the filename is configurable. I figured that would be the end of it: configure a ConsoleAppender in the config file and log the access logs to the console. In my environment those messages would be visible along with the other logs. Sadly, it was not that simple at all.

First, Logback complained that the "common" pattern I was using in the configuration (literally the default) was invalid. It turns out that Spring Boot reconfigures Logback so hard that it doesn't recognize its own default configuration anymore. If you check out Agony Forge's GitHub you can find my logback-access.xml in which I redefine all the default conversion patterns in addition to including Spring Boot's default configuration for all the fancy colors and stuff that it does. Fun stuff!

Next, Logback complained about a ClassCastException because it couldn't cast an ILoggingEvent to an IAccessEvent. After a couple more hours of reading articles, documentation and poking through the code for Logback I realized there are two distinct types of log messages in Logback: regular logging events and "access" events. There are also two distinct kinds of Appenders for those events that are not compatible with one another. Because many applications have a very high volume of "access" events, they never built a ConsoleAppender for access events. If your website has very much traffic, it would be too slow. I get that logic and agree with it in principle, but in this case it very much does not apply.

So I wrote my own AccessLogAppender in Agony Forge. It's very simple, and loosely based on the ConsoleAppender. The clever part is that when it receives an IAccessEvent it formats it according to the configuration and then... logs it to the regular game's logger. Now I'm sure you're asking, "isn't that inefficient?" and "won't that be slow?" and the answer is yes. If you have a large volume of HTTP traffic you do not want this to be turned on. In my case there are two reasons it's not a big deal:

  1. The amount of traffic I am receiving is basically zero, and I'm just using it for debugging network issues.
  2. Once a player has logged in and moved over to Websockets, they don't generate any more access log messages. So most of the time even on a busy MUD there still wouldn't be a lot of these messages.

Fortunately there are a couple of ways to turn it off if you need to.

  1. It logs at DEBUG so you can just turn it off in application.yaml.
  2. If that's not enough, you can turn it off by disabling the LogbackValve in TomcatConfiguration.

Anyway, that was my adventure with Logback in Spring Boot. Given how few articles I could find about this stuff that weren't either extremely basic or ten years old, I imagine this could be helpful to someone out there.

Go to Comments

The Devil Wears Enums

2024-12-19

I got the item editor to work!

Creating a really big sword in the item editor!

Isn't that so cool?

Agony Forge has both items and item prototypes. When you edit, you're editing the prototype. After that, any item instance created from that prototype will have the new properties, but items that were created before will not be changed. Of course, the editor can also create new prototypes as you can see in the video. I'm pretty excited that it's working and there were a few interesting Hibernate shenanigans to overcome just to get here.

One of the difficulties was wear slots. In the video you can see that I set my new sword so you can equip it in your right hand by choosing it off a list, and that selecting it toggled it from "false" to "true". But how is that implemented?

I started out with a fairly naive approach. I defined a WearSlot enum and did this in the MudItem class:

@ElementCollection
@CollectionTable(name = "mud_item_wearslot_mapping",
joinColumns = {@JoinColumn(name = "item_id", referencedColumnName = "instance_id")})
private Set<WearSlot> wearSlots = new HashSet<>();

What this mess of annotations does is create an extra table with a row for each slot mapped to the item's ID. With 19 wear slots that means every item in the game gets 19 rows in this table. Now, I don't have any players on my MUD yet and I haven't built more than two or three items at a time, but back in the day our MUDs used to have several thousands of items in memory and usually many more than that offloaded to disk in player files and area files. Multiply that by 19 and your database will be starting to get to a pretty respectable size. Not to mention that there's a table of items and a table of prototypes, each of them with this additional large table for wear slots.

But that wasn't even the biggest problem with this setup. The biggest problem was that for some reason changing the wear slots and saving the item did not update the wear slot table. I read a bunch of documentation and articles about it and they all insisted that operations would cascade from the main item to the ElementCollection attached to it, but in this case it absolutely did not. I still don't know why, because for the aforementioned reasons I decided to start over with an entirely different approach.

It turns out I had already solved this problem elegantly six years ago when I was working on an earlier incarnation of this same MUD. Fortunately the much cleverer six-years-ago-me saved that code in GitHub and it was still there for today-me to find. The basic idea was to use a 64 bit integer to store the state of each slot. No extra tables, just one big integer to represent a bit vector. This works because each wear slot can either be "true" or "false" and there is no other information that needs to be stored about it. For more complex objects of course, you'd want to make an entity and use a regular @OneToMany type of relationship.

First, I defined a completely empty PersistentEnum interface. Then, an EnumSetConverter (abridged slightly - see the GitHub link at the bottom of the article for the whole thing). You could skip this step and just allow any Enum, but I did this so that I have a way to visually identify which enums I'm using in the database. If I make a mistake and try to use the wrong enum, the compiler will tell me.

public abstract class BaseEnumSetConverter <E extends Enum<E> & PersistentEnum> implements AttributeConverter<EnumSet<E>, Long> {
    private final Class<E> klass;

    public BaseEnumSetConverter(Class<E> klass) {
        this.klass = klass;
    }
    
    @Override
    public Long convertToDatabaseColumn(EnumSet<E> attribute) {
        long total = 0;

        for (E constant : attribute) {
            total |= 1L << constant.ordinal();
        }

        return total;
    }
    
    @Override
    public EnumSet<E> convertToEntityAttribute(Long dbData) {
        EnumSet<E> results = EnumSet.noneOf(klass);
    
        for (E constant : klass.getEnumConstants()) {
            if ((dbData & (1L << constant.ordinal())) != 0) {
                results.add(constant);
            }
        }
    
        return results;
    }
}

An EnumSetConverter is responsible for taking an EnumSet and computing a Long based on which enums exist in the set. It uses the built in ordinal of the enum (a unique sequential number the compiler assigns to each value in the enum) which is why the PersistentEnum interface can be completely empty.

There are a couple of limitations with this approach: if your enum has more than 64 values it won't fit in a Long, because each value is represented by one bit in the 64 bits of the Long. Also, if you add more values in the middle of an existing enum, the ordinal values could get reassigned and any saved EnumSets could load incorrectly. Adding new values at the end should be safe as long as you don't change the order of the existing values. In my case I'm not concerned about breaking changes because my database gets deleted every time I run a build!

Now, to use the converter I defined an enum for WearSlot that implements PersistentEnum. In it are a bunch of locations that you can wear equipment on, and the descriptions of them that the game can use to build sentences like "You wear a lampshade on your head." At the bottom of the enum I also define a converter class for it:

public static class Converter extends BaseEnumSetConverter<WearSlot> {
    public Converter() {
        super(WearSlot.class);
    }
}

Finally, I stuck one of these on MudItem to represent all the wear slots:

@Convert(converter = WearSlot.Converter.class)
private EnumSet<WearSlot> wearSlots = EnumSet.noneOf(WearSlot.class); // noneOf creates an empty set, all bits set to 0

EnumSet has been in Java since 1.5 and is super cool if you haven't seen it before. It holds a set of enums represented internally as a bit vector. It gives you some really handy methods to create sets, add, remove and toggle the "bits". But the best part is that with the EnumSetConverter Hibernate can easily convert the EnumSet into a Long and back again.

This entirely eliminated the need for additional tables, for joins, and sidestepped the mysterious problem with Hibernate not updating my ElementCollection. It works like a charm. Check out the code for MudItem in the Agony Forge GitHub for an example of how it works.

Go to Comments
[ Copyright 2024 Scion Altera ]