Command & State management in Mud Designer

The previous version of the Mud Designer had a pretty good State system. The user would enter a command, the command would perform an action and the state would render the results to the screen. I spent a lot of time over the last couple of weekends trying to refactor the state system and add a couple new features.

Command Searching

I provided the player with a StateManager property, that the object can now access. This is used to try and find a command suitable to what the user typed, and execute it. In the new version of the Mud Designer, the delegate for receiving the user input, will call StateManager.PerformCommand which will try to find a command that can be used.

There are several ways that a Command can be found and executed. The manager makes an attempt to see if the command is stored in a collection of commands that the player owns. If it is, then the command is executed. If not, then the manager checks to see if the command has any short hand attributes. A command can be decorated with a short hand like this:

[ShorthandNameAttribute("Walk", "mv")]
public class WalkCommand : ICommand
{
}

This allows the actual command name, to vary from that of the class name. While you can provide any name, it is strongly recommended that builders implementing their own commands stick with a convention that the Mud Designer uses. That convention is to name your Command, with the command word appended. Like LookCommand, WalkCommand, TellCommand etc. With the attribute, you can shorten the name to just Look, Walk or Tell. You can shorten the name even further using the secondary argument. The three aforementioned commands can be shortend to l, w and t if you want to.

If no command is found matching our convention (user enters Walk, so we look for WalkCommand), then the manager will check all of the commands for a ShorthandNameAttribute. The ones that contain this attribute are checked. If any of them have a command name or shortened command that match the users input, then we have a match. Otherwise we continue our search.

The Stack.

The manager now has a Stack for commands. If a command is not completed, it pushes the command on to the stack and resumes control to the players individual game loop. If the command is completed, the command is popped off of the stack.

Once the manager has failed to find a command that matches the input provided by the user, it hits up the stack. If the stack contains a command, it pops it off and executes the command.

When a command's .Execute() method is finished, it can set a IsIncomplete property to true. If the property is set to true, it lets the State Managerk now that the command still has more steps to perform before it is really finished. Something like a User Login might require multiple steps (username and password entry). While it is possible to fetch the data directly from the user while in the command, this is now frowned on as it holds the thread up. It is better to return control to the caller and wait for the user to enter data in to the main player loop. When the new data is entered, the State Manager will pass that data back to the same command, which can then finish off what it needs to.

When a command is marked as incomplete, it is pushed on to the stack. When the command is executed, it is popped off of the stack. Only commands that must be preserved for additional actions are placed in the stack.

Example Command:

The following is an example of a single action command.

using System;
using MudEngine.Engine.Core;
using MudEngine.Engine.Networking;

namespace MudEngine.Engine.Commands
{
    [ShorthandName("Disconnect", "/dc")]
    public class DisconnectCommand : ICommand
    {
        public string CommandInput { get; private set; }

        public bool IsIncomplete { get; }

        public void Execute(GameObjects.Mob.IMob mob, string input)
        {
            if (mob is ServerPlayer)
            {
                var player = mob as ServerPlayer;

                mob.Send(new InformationalMessage("Disconnecting."));
                player.Disconnect();
            }
        }
    }
}

Pretty straight forward. You can see how we set our shorthand names, send a message to the player that we are disconnecting and then disconnect them. The player can type Disconnect or /DC and the game will disconnect them from the server.

Example Command with State

Now we want to maintain state in our command. Let's create a simple "Provide your name and age" command used during character creation.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MudEngine.Engine.Core;
using MudEngine.Engine.GameObjects.Mob.States;
using MudEngine.Engine.Networking;

namespace MudEngine.Engine.Commands
{
    public class CreateCharacterCommand : ICommand
    {
        int state = 0; // 0 = uninitialized; 1 = username entered, 2 = age entered.
        public string CommandInput { get; private set; }

        public bool IsIncomplete { get; private set; }

        public void Execute(GameObjects.Mob.IMob mob, string input)
        {
            // First execution of the method.
            if (!this.IsIncomplete)
            {
                state = 0;
                mob.Send(new InputMessage("Please enter your name"));
                this.IsIncomplete = true;
                mob.StateManager.SwitchState< ReceivingInputState>();
            }
            else if (this.state == 0)
            {
                this.CommandInput = input;
                mob.Send(new InformationalMessage(string.Format("Hello {0}", this.CommandInput)));
                mob.Send(new InputMessage("Please enter your age"));
                this.IsIncomplete = true;
                this.state = 1;
                mob.StateManager.SwitchState< ReceivingInputState>();
            }
            else
            {
                // Out put the age from current input along with name from previous command input.
                mob.Send(new InformationalMessage(string.Format("You are {0}s old {1}", input, this.CommandInput)));
                this.IsIncomplete = false;
                this.state = 2;
                mob.StateManager.SwitchState< LookState>();
            }
        }
    }
}

As you can see in here, we maintain some simple state to determine where in our user data input state we are. The state manager will execute this command once, then determine that it is not completed and push it to the stack. When the user enters "Bob" for a user name, the State Manager will not find a command matching "Bob" and will pop the previous command off the stack and execute it.

The benefit here is that the player can type "Help", which the state manager will find. So the user can be in the middle of a command, and piggy back on top of it to execute another. This will work really well going forward and allow for some complex command chaining as well.

Conclusion

While commands at the moment can be executed without a ShothandNameAttribute decorated on the class, I am considering making this a requirement. By requiring all commands be given at least a name (shorthand version optional), I can allow the engine (and the commands wrote for it) to execute internal commands. A "CreateCharacter" command would not be executable from a user, since there are now ShorthandNameAttributes associated with it. Yet, your Login command could request that the players StateManager perform the command.

mob.StateManager.PerformCommand(new ReceivedInputMessage("CreateCharacter"));

This isn't wrote in to the engine yet, but this is something I am considering before shipping.