Not a member yet? Register for full benefits!

Username
Password
MUD Pies: Part 2

Here We Go Again!

Welcome to part two of "MUD Pies". First and foremost, I apologise for this being so long in the making, and thankyou to everyone who sent encouraging emails and enquiring when it would be finished. Here it is. If for whatever reason, I am late with the series again, what you learn in this tutorial is enough for you to go off on your own MUD (this is the end of the substantial network code).

Once again, I strongly recommend downloading the code that comes with this article. It will make it easier for us both. I am working off the MSVC compatible version, but I do not guarantee that the code for this part is MSVC compatible.

As with any code, bugs will arise. A few were found within weeks of the first article going public, and have been changed in the downloadable code and the article but I am uncertain about the printable-version. In any case, none were major changes. These new bugs are more substantial and need to be dealt with. The worst of which is a big memory leak because I forgot to delete the server class. Add in a delete CServer::GetServer(); at the end of the main function in CApp. Next, the window is not being invalidated properly, so updates are not being displayed as expected. Add in InvalidateRgn(hWnd, 0, 0); in response to a SM_WINSOCK message in CApp::WndProc(). Finally, to make it easier later, CSock::GetText(char* dest, int length) now returns the number of characters read from the buffer (so we don't have to look at the string again.

Other aesthetic changes you may notice is that the macro GETSERVER, and the function CApp::PaintWindow() have been added (and the response to a WM_PAINT message has been moved into it).

That's the end of the minor changes. The major changes that are covered in this article are:

  • Addition of a string class to make our lives a whole lot easier
  • Grabbing strings from the socket
  • A finite state machine (FSM) that handles what the user is currently doing (entering a password, talking, etc)
  • Saving and loading files to and from disk (with a crash course in TinyXML)

A String Class

Note from the future: I no longer recommend using my class (unless you are just using the accompanying code). Use the Standard Template Library string class instead.

Rather than handle all the complexity of C strings again and again (and explain them over and over) I have taken a string class from another of my projects and used it for this tutorial. You should reuse code as often as possible, and always try and write code (especially utility-like classes - like string classes) that can be used in other projects. Searching for nul characters, handling memory and such gets tiresome quite quickly.

Also note that this class is commented to be auto-documented by doc++, doxygen, etc.

I will now give a quick explanation of its internals. And seeing I won't get another chance, an overview of C strings.

C strings work by having a pointer into memory at the start of the string. Instead of holding the length of the string, the string ends in what is known as a nul character. The actual value of this character is zero, and to create one in a string use ''. This really sucks, because whenever you want to do much manipulation of the string, you have to sequentially access it right til the end to know how long it is.

For instance, the string "Andrew", a pointer to the first letter exists, and then memory after that pointer is part of the string up until the nul terminator. So when viewing the 8-bit character values in memory, you will see 'A', 'n', 'd', 'r', 'e', 'w', 0. The actual byte length of that string is seven.

If you have looked at your compiler's documentation of their runtime library, you may have seen different memory manipulation functions. There is a function strcpy that copies a string. It is inefficient and hard to use, because it must read the string's contents to find its nul character. It also requires that there is a buffer waiting with enough memory to hold that string. This can get painful quickly. There is a similar function called memcpy that takes a length and can just grab memory from one place and throw it into another quickly (because it doesn't have to read it). The same is the case with other functions that start with str or mem. The mem function takes a length, whereas the str function searches for the nul character.

So wouldn't it be easier if could keep track of the string's length. That is essentially what the string class I will use does. It has both has a nul character (so the string can quickly be used by functions that need a string) and a length in bytes (that includes the nul character). These are defined as follows:

class AString
   {
   private:
   char* m_strData;
   int m_iLength;
   };

Now, we want to do operations on this string. For instance, we want to set strings to each other, add strings together, compare strings, etc. I will cover these basic operations briefly here, for their exact implementation, see the accompanying source code.

First of all, constructing the string class. We have two variables to set, a string and a length. Length is simply a matter of setting the length variable to the appropriate value. Setting the string involves copying memory.

For starters, we don't have any memory set aside for our string. We must use the new operator to create an array of characters of the appropriate length:

m_strData = new char[m_iLength];
And then copy the data into it using memcpy:
memcpy(m_strData, str, m_iLength);

The function memcpy takes three arguments. The first is a pointer to the start of the destination memory, the second is the pointer to the start of the source memory, and the third is the length to copy in bytes.

Deconstruction is a simple matter of releasing the string's memory using the delete[] operator. This frees an array of objects in memory (in this case, they are characters taking a single byte each).

An assignment operator can be implemented by combining the above two; first by freeing the old string, and then by copying in the new string. For those who do not know how to create an operator, here is how it's done. Note that different instances of the same class can access each other's privates. The variable rhs refers to the right hand side of the operator symbol.

AString& AString::operator= (const AString& rhs)
   {
   delete[] m_strData;
   m_iLength = rhs.m_iLength;
   m_strData = new char[m_iLength];
   memcpy(m_strData, rhs.m_strData, m_iLength);
   return *this;
   }

Appending one string onto another is not much harder. Note that when you have two strings, you also have two nul terminators, whereas when you create the final one, you only need one. This is why we reduce the length by one here.

AString& AString::operator+= (const AString& rhs)
   {
   char* oldstring = m_strData;
   m_strData = new char[m_iLength + rhs.m_iLength - 1];
 memcpy(m_strData, oldstring, m_iLength);
   memcpy(m_strData + m_iLength - 1, rhs.m_strData, rhs.m_iLength);
 m_iLength = m_iLength + rhs.m_iLength - 1;
   delete[] oldstring;
   return *this;
   }

And with the += operator, we can cheat when we make our + operator:

AString& AString::operator+ (const AString& rhs)
   {
   AString* newstring = new AString(*this);
   *newstring += rhs;
   return *newstring;
   }

That covers the more complicated operations. Other operations that involve string comparisons and the like are easy to do, view the source if you are unsure of the functions to use.

Just finally, when you want to have a string compiled into your program, you must do it slightly differently than expected. For a C string, you can simply type it in between inverted commas, for an AString; use the following macro around a C string. This also avoids a warning message ("Temporary used for parameter").

#define A_STR(str) AString(str, sizeof(str))   
The Receiving Buffer

Now that we have a string class, it should be relatively easy to add in more string related functions. You may notice that I have changed a couple of functions over to use AString. This is not a big deal as the functionality is the same.

The reason we need a receiving buffer is because some clients will send us one or several characters at a time, rather than one line at a time. Namely this client would be telnet, and this is important, because we will be making our MUD telnet compliant.

Under normal operation, the buffer will just add any data that is received by the socket to its end. You don't want people talking one letter at a time, so the buffer is only "executed" (put into action) when it contain a carriage return (enter) character ('/r'). When this happens, all the text up until the enter is taken and executed, and then the buffer is reset to contain whatever was left at its beginning.

If the case occurs that the buffer fills up before a enter character is added, then we must handle it. As the buffer is full, there is no way for the user to add in a enter character, so a suggested way to handle this situation is to clear the buffer entirely, and give the user a message about their "flooding" (sending excess text).

It is possible to have a buffer of infinite length, but our variable that keeps track of buffer would overflow well before we reached infinity, and also, we don't want the user to be able to generate out of memory errors (that can bring down the server or the machine itself) by having multi-megabyte buffers in use.

For our buffer, add the following to CUser:

bool ProcessText();
   char* buffer;
   unsigned int buffersize;
And we also throw in a:
   #define MUD_LINE_MAX 1024

The function ProcessText is to handle getting text from the socket and appending it to the buffer. It is called whenever the user class receives a message that text is waiting on the socket. Note that the buffer is not one of our shiny new AString classes, this is because we do fancy stuff directly to the memory that the buffer involves, and we don't need to add that kind of functionality to AString. Also note that defiantly isn't the fastest way of handling this, but it is simple so it can be understood easily. If you want to get really fancy, then you could go and use a specialised string library.

Note that buffer is initialised in the constructor and is always an array of MUD_LINE_MAX charcters (althought it is reinitilised throughout the code).

Now let me step you through the ProcessText function:

First of all, we need to grab the incoming text from

buffersize += socket.GetText(buffer+buffersize, MUD_LINE_MAX - buffersize);

We then check if the updated buffer contains an enter character. The function strchr returns the position of that character, or NULL if it does not exist. If the buffer does not contain an enter character, and it is full, then reject it and create a new one.

char* enterpos = strchr(buffer, 'r');
   if(enterpos == NULL)
   {
   if(buffersize >= MUD_LINE_MAX-1 )
   {
   delete[] buffer;
   buffer = new char[MUD_LINE_MAX];
   buffersize = 0;
   return false;
   }
   }

On the other hand, if we do have an enter character we can execute the buffer.

else
   {
   AString execbuf;
   do
   {
   // ...
 Execute(execbuf);
   }
   while( (enterpos = strchr(buffer, 'r')) != NULL ); 
   }

We need to fill in that do statement. We need to add functionality to get out the data that we want to execute, and also to remove that same data from the existing buffer. The loop above will keep doing it until there are no more enter characters. This is because as well as some clients being able to send less than a line at once, some can send more than a line at once.

So consider what we have at the moment. We have a pointer to the beginning of the string and a pointer to the location of the enter character. Between these two pointers is the string we want to get at.

enterpos[0] = '';

This changes the character to a nul terminator. We can then use those string functions mentioned before (that search up until a nul terminator) to extract the string. Our AString class already does this in one of its constructors, which is good because we want our execute buffer to be an AString.

execbuf = AString(buffer);

We want to make a new buffer that has the end of the old one in it. This is where we make it, and then fill it from enterpos onwards. We increment the pointer by one for avoiding the nul terminator we inserted earlier. Some clients can also add a newline character ('n') so we jump over that too if need be (it will be right after the ('r').

char* newbuffer = new char[MUD_LINE_MAX];
   memcpy( newbuffer, enterpos + ((enterpos[1] == 'n')?2:1), buffersize - ( enterpos
    + ((enterpos[1] == 'n')?1:0) - buffer ) );

If you haven't seen it before, the ?: ternary (three part) operator works like an if statement. If part one evaluates true, part two evaluates, otherwise part three evaluates. In this case parts two and three simply return a number.

Now we want the size of the new buffer:

buffersize -= ( enterpos + ((enterpos[1]=='n')?2:1) - buffer );

Finally, we delete the old buffer and replace it with the new:

delete[] buffer;
   buffer = newbuffer;

Now, before the next loop, you need to execute the buffer you have extracted. This will be in another member function of CUser.

To start with, we shall just make the server transmit that string to every connection. Before we do that, we need two new functions. In the class definition of CUser:

inline void SendText(AString& str) {socket.SendText(str.GetString());}

And then add the following function to CServer:

void CServer::Broardcast(AString& str)
   {
   AString buf = str+A_STR("rn");
   for(CUser* curuser=rootuser; curuser != NULL; curuser=curuser->next)
   {
   curuser->SendText(buf);
   }
   }

So now we add our execute function. This is just to test it works, it should look something like this:

void CUser::Execute(AString& str)
   {
   if(str.GetLength() <= 1)
   {
   return;
   }
   GETSERVER->Broardcast(str);
   }

Next, instead of simply sending the string out, that statement will be filled with our finite state machine.

Finite State Machines

Finite State Machines (FSMs) are frequently and most complexly used in artificial intelligence (AI) programming. States are used for different tasks for the AI, such as the "attack" state, the "defend" state, or the "find ammo" state.

FSMs are also used in a much more simplistic manner to handle different states of other things. Just as a light switch has an on state and an off state, a game may have a "menu" state, a "play" state, a "demo" state, etc.

Finite state machines are usually event based to some degree or another. The most common events are the "leave state" and the "enter state" events. Our FSM will be simplistic enough to not have to specify enter and leave events specifically, the actions normally performed by these events will be handled in our function, Execute. The Execute function can be described as an event handler, as it is called in response to an incoming line event.

For our FSM, we need to enumerate following states:

enum UserState
   {
   US_NONE,
   US_TALKING, // User talking
   US_ENTERUSERNAME, // User entering a user name
   US_ENTERPASSWORD, // User entering a password
   US_CREATEPASSWORD1, // User creating password 
   US_CREATEPASSWORD2 // User confirming password
   };

Then add a variable, UserState state to CUser to keep track of the current state for that user.

Now we have our states, we can make the following test to see if a user is logged in:

inline bool LoggedIn() {return !(state == US_ENTERUSERNAME 
	|| state == US_ENTERPASSWORD || state == US_CREATEPASSWORD1 
	|| state == US_CREATEPASSWORD2 || state == US_NONE);    }

You'll want to call this function and check if a user is logged in before you go and broadcast text to them - add this check to CServer::Broardcast().

So we want to do a different thing when we receive an incoming-line event depending on what state we are in, so add a switch statement in place of that call to CServer::Broardcast(), which is dependent on the value of state, and add a case statement for each possible value.

TinyXML Crash Course

I'm sorry for anyone looking forward to me going retro and making some sort of hacked-together file format. After much mucking around and testing of different ways of doing files, XML was considered the best (because it needs to be easy to explain and can have extra values added later in the series without making the older files unreadable). Writing an actual XML parser is out of the question for this article series so TinyXML was chosen, as it is nice and free and easy to use. You are welcome to use any other data storage method. Perhaps you want to get really tricky and have your MUD use SQL?

I am going to assume you know how to write XML - it should be a breeze if you have had experience with any other ML (eg: HTML). If you have no idea, Richard Fine has written an excellent article on XML in games here: http://www.gamedev.net/reference/programming/features/xml/.

You can download TinyXML from here: http://www.grinninglizard.com/tinyxml/.

You will need to have the documentation on hand if you don't know what a function call does (like you needed the Win32 SDK to look up details in the last tutorial).

First job, add TinyXML to your project by adding the following files: "tinystr.cpp" "tinyxml.cpp" "tinyxmlerror.cpp" "tinyxmlparaser.cpp". Check it compiles (it should) and fix any errors (I know, famous last words): It returns 0 in some places where it should return false, which Borland's compiler doesn't like.

Here is a quick look at what our XML file should look like:

<muddata>
   <userlist>
   <user name="AndrewRussell" pass="test" />
   <user name="test" pass="test" />
   </userlist>
   </muddata>

I've added the class, CDataFile, which will handle TinyXML stuff. It contains a member of the type TiXmlDocument, which is what TinyXML represents an XML file as.

I've also added

 TiXmlElement* UserListElement;

to quickly get a pointer to the user list. The constructor of CDataFile will load the XML file, and the deconstructor will save it back to disk again.

I've added the following function as well:

TiXmlElement* CDataFile::GrabUserByName(AString& name)
   {
   // Grab the first user node
   // retval will be returned as and if we find the user with the correct name
   TiXmlNode* retval = UserListElement->FirstChildElement("user");
 while( retval != NULL )
   {
   // is it an element?
   // does it have the same name as we want?
   if(retval->ToElement() && (AString(((TiXmlElement*)retval)->Attribute("name",
    NULL)) == name))
   {
   return (TiXmlElement*)retval;
   }
 // Grab the next return value
   retval = UserListElement->IterateChildren("user", retval);
   }
 return NULL;
   }

As you can see, this will go through the XML file, and search out a user with the user name we want.

Finally, I've added
TiXmlElement* userdata;
to CUser, so the user can keep track of its XML element.

Now that you've inserted XML functionality into your application, now make sure it still compiles.

Putting it all Together (FSM part 2)

Now we can load and save files, we can finish off our Finite State Machine.

Here is a diagram of how our Finite State Machine will operate:

Here is a list of the different states:

US_ENTERUSERNAME
All input is the user attempting to enter a user name. Here I only check it's not longer than 64 characters. You may want to check for spaces, symbols, etc. Once a valid user name is entered, then search for it in the user database. If it's found, let them try to enter a password. If it cannot be found, then get them to create a user.

US_ENTERPASSWORD
If they entered an existing user name, ask them to enter the password for that user, and match it up with the database. This is fairly straightforward.

US_CREATEPASSWORD1
Get them to enter some text for their password. Again, you may want to add checks to make sure they're not trying to send you symbols, colour codes, and other such garbage.

US_CREATEPASSWORD2
Here we check if the password they entered matches their first attempt to enter a password. If it does, log them in, and also add them to the XML database.

US_TALKING
Simply tag their name onto, and then broadcast whatever they type, to the rest of the users.
Here is the code for all of that:

void CUser::Execute(AString& str)
   {
   // check for 0 length string size of 1 for nul terminator
   if(str.GetLength() <= 1)
   {
   return;
   }
 switch(state)
   {
   case US_NONE:
   break;
 case US_TALKING:
   CmdSpeak(str);
   break;
 case US_ENTERUSERNAME:
   // Check that lengh is small enough (don't want it too big)
   // An excersise - make length configurable in mud.xml (remember to then make
    length only affect new user names, we don't want making it smaller stopping
    people logging in)
   if(str.GetLength() < 64)
   {
   userdata = GETSERVER->datafile.GrabUserByName(str);
   if(userdata != NULL) // does the user exit
   {
   SendText(A_STR("Welcome Back ") + str + A_STR("!rnEnter Password:rn"));
   state = US_ENTERPASSWORD;
   }
   else // user dosn't exit - create and ask for password
   {
   SendText(A_STR("User ") + str + A_STR(" Does Not Exist - CreatingrnEnter
    Password:rn"));
 userdata = new TiXmlElement("user");
   userdata->SetAttribute("name", str.GetString());
   state = US_CREATEPASSWORD1;
   }
   }
   else
   {
   SendText(A_STR("Too Long! Try again...rn"));
   }
   break;
 case US_ENTERPASSWORD:
   // Check the password matches
   if(str == AString(userdata->Attribute("pass")) )
   {
   // Now we can start talking
   state = US_TALKING;
   username = AString(userdata->Attribute("name"));
   SendText(A_STR("Logged In!rnrn"));
   GETSERVER->Broardcast(A_STR("Please welcome ") + username + A_STR("
    to the server!rnrn"));
   }
   else
   {
   SendText(A_STR("Password does not match - try again...rn"));
   }
   break;
 case US_CREATEPASSWORD1:
   if(str.GetLength() < 64)
   {
   userdata->SetAttribute("pass", str.GetString());
   state = US_CREATEPASSWORD2;
   SendText(A_STR("Confirm Password:rn"));
   }
   else
   {
   SendText(A_STR("Too Long! Try again...rn"));
   }
   break;
 case US_CREATEPASSWORD2:
   if(str == AString(userdata->Attribute("pass")) )
   {
   // User account created sucessfuly - add to database
   // This transfers the object to tinyxml
   GETSERVER->datafile.UserListElement->LinkEndChild(userdata);
 // Now we can start talking
   state = US_TALKING;
   username = AString(userdata->Attribute("name"));
   SendText(A_STR("Account Created! Logged In!rn"));
   GETSERVER->Broardcast(A_STR("Please welcome the new user, ") +
    username + A_STR(", to the server!rnrn"));
   }
   else // No Match
   {
   SendText(A_STR("Password does not match - try again...rn"));
   }
   break;
   }
   }

Before we finish up, I'll describe what owns the XML element where, as this could be somewhat confusing.

In US_ENTERUSERNAME, the XML variable is set to NULL, and it knows nothing about the XML element. If it goes into US_ENTERPASSWORD, then it gets a pointer to an XML element that is owned by, and constructed and deconstructed by TinyXML. However if it is a new user account being created, the XML element we create is owned by us, and not by TinyXML. We are responsible for deleting that element. That is - unless we register it with TinyXML, which will make TinyXML in charge of deleting it when it's done, and also, for saving it to the XML file. You can see it being registered in the code that handles US_CREATEPASSWORD2. If the user is in the talking state, we are guaranteed that TinyXML owns the XML element (unless we made some sort of coding mistake).

Final Notes

It has been a long time since I started writing this article, and even longer since I released the first one. Real life makes it quite hard to have time to do these things. Apologies to all those who have sat around waiting for it, and thankyou to the many people who emailed me telling me to get on with it.

Most importantly, in this time, I have come to use the STL (standard template library) almost religiously (where as previously I used it rarely, if ever). I highly recommend learning how to use it.

Anyway, you now have a functional chat server. It is now possible to continue programming your MUD server on your own, and I highly recommend doing so. Later tutorials are not going to be based on the code as much, so are unlikely to break anything.

When I say "functional", I do not recommend using it for any sort of public server. There are some huge security holes. Most importantly, we don't check for things like quote marks in user names, which, when saved directly to TinyXML could very easily muck up the XML file.

I will address these security problems in a later article.

Until next time,
Happy coding,

Andrew Russell.

Staff Comments

 


.
Untitled Document .