Handler Design

[ LiB ]

The SimpleMUD has several connection handlers. Almost all of them are going to be related to using the player and the player database classes, so you'll need to have a firm understanding of the general design. Within this chapter, there is a section on each handler that will be used. Some of the handlers are outlined in this chapter and then fleshed out later in the book.

There are three handlers within the game: a logon handler, a game handler, and a training handler.

Logon Handler

First, when you log into the game, you'll be prompted with the logon handler, which is similar in purpose to the logon handler found in SimpleChat from Chapter 6. This time the handler has more responsibility, since it needs to support more options.

First and foremost, the logon handler has to accept two types of users: new users and existing users. For existing users, it needs to check passwords and check to see if the user is already logged in before it can let a connection into the game. For new users, it needs to validate usernames and passwords and add them to the database.

It's always a good idea to be able to picture a process, so I've drawn a flowchart of the logon handler functions in Figure 8.9.

Figure 8.9. Flowchart of the logon handler functions.

graphic/08fig09.gif


From Figure 8.9, you can see that the logon handler starts off by asking the player his name . At that point, the player has two options: entering "new" to indicate that he's a new player, or entering his existing username. If the player enters anything but "new", and what he entered into the handler doesn't exist within the player database as an existing name, that is considered an invalid response, and the state remains the same.

New users are prompted to enter their desired names and are notified if those names already exist. If the names don't exist, the users are prompted for their desired passwords; when the users enter their passwords, they are taken to the training handler, where they modify their stats.

Existing users are prompted for their passwords; when they enter the appropriate password, they are taken to the game.

Note that whenever five invalid responses are received by the logon handler, it automatically disconnects the player.

Logon States

A connection that is logging on can be in four different states at any given time. These four states are represented by the four solid boxes within Figure 8.9:

 enum LogonState {     NEWCONNECTION,           // first state     NEWUSER,                 // new user; enter desired name     ENTERNEWPASS,            // new user; enter desired password     ENTERPASS                // existing user; enter password }; 

Logon Data

The SimpleMUD: Logon class has several pieces of data attached to it:

 class Logon : public Telnet::handler { protected:     LogonState m_state;     int m_errors;     // how many times has an invalid answer been entered?     string m_name;    // name     string m_pass;    // password }; 

Every logon handler stores the state, the number of errors a connection has made, and the name and password that the connection has entered.

Logon Functions

Here is a listing of all the functions that a logon handler has:

 void Enter(); void Leave(); void Hungup(); void Flooded(); static void NoRoom( Connection<Telnet>& p_connection ); void Handle( string p_data ); void GotoGame( bool p_newbie = false ); static bool AcceptableName( const string& p_name ); Logon( Connection<Telnet>& p_conn ); 

The first six function names should be familiar to you, because they are the standard handler functions inherited from the Telnet::handler class to handle events from a ConnectionManager .

The GotoGame function converts a connection from the logon handler to the SimpleMUD::Game handler, and the AcceptableName function determines if a username is acceptable. The last function is a constructor.

Hanging Up, Flooding, and No Room

I'd like to cover the simpler functions first, if I may. Essentially , the logon handler does not care too much if a connection floods or hangs up; the connection manager automatically disconnects that connection when those situations occur, and the logon handler doesn't really need to do anything special.

Nevertheless, it always helps to keep a log of what is happening on your server, so those two functions record the events in the user log:

 void Hungup() {     USERLOG.Log(         SocketLib::GetIPString( m_connection->GetRemoteAddress() ) +         " - hung up in login state." ); }; void Flooded() {     USERLOG.Log(         SocketLib::GetIPString( m_connection->GetRemoteAddress() ) +         " - flooded in login state." ); }; 

These functions retrieve the IP address of the offending connection and write that information to the user log.

When there's no more room left, a message needs to be sent to the connection trying to join:

 static void NoRoom( Connection<Telnet>& p_connection ) {     static string msg = "Sorry, there is no more room on this server.\r\n";     try {         p_connection.Send( msg.c_str(), (int)msg.size() );     }     catch( SocketLib::Exception ) {         // do nothing here; probably an exploiter if sending that data         // causes an exception.     } } 

The function must physically send the data to the socket using the Connection::Send function instead of queuing it up using Connection::BufferData , since I can't rely on the ConnectionManager to send queued data. (The connection isn't managed by the connection manager; it is immediately discarded and closed.)

Since I am calling Connection::Send here, there is a remote possibility of an exception being thrown, so I need to be ready for that, too. Because the connection is closing anyway, the catch block simply catches the exception and ignores it.

Leaving and Entering

Whenever connections leave or enter this state, the Leave and Enter functions are called. Luckily, the logon handler doesn't have to clean up after any connections when they leave (since they haven't been added to the game yet), so you can leave it as an empty function:

 void Leave() {}; 

On the other hand, when connections enter the game, the logon handler needs to send it a welcoming message:

 void Enter() {     USERLOG.Log(         GetIPString( p_connection.GetRemoteAddress() ) +         " - new connection in login state." );     p_connection.Protocol().SendString( p_connection,         red + bold + "Welcome To SimpleMUD v1.0\r\n" +         "Please enter your name, or \"new\" if you are new: " + reset ); } 

It's not an imaginative welcome message, but it works. A more flexible method of displaying a welcome message is to load a message from a text file on the server, and print that, but that's just a special feature that's not essential to the game. Feel free to implement it if you wish.

NOTE

A more advanced MUD would put IP-address detection into the con nection process, so one person couldn't flood your MUD with connections.

Handling Commands

Remember what I showed you in Chapters 5 and 6: whenever a full command is received by a protocol policy object, it passes the command onto the connection's current handler. The Logon::Handle function is called whenever a connection is using the Logon class as its current handler.

Essentially, this function performs the actions that I described earlier and illustrated within Figure 8.9. Since it's a relatively large function, I'm going to break it up into a few chunks , to describe it better. Here's the first part ( p_data is the string that the user typed in):

 void Logon::Handle( string p_data ) {     if( m_errors == 5 ) {         m_connection->Protocol().SendString( *m_connection, red + bold +             "Too many incorrect responses, closing connection..." +             newline );         m_connection->Close();         return;     } 

The previous section of code detects if five errors have occurred in the login process, and if so, the connection is informed of this and closed. This prevents people from endlessly trying passwords, trying to break into an account.

The next part of the code handles brand new connections:

 if( m_state == NEWCONNECTION ) {         if( BasicLib::LowerCase( p_data ) == "new" ) {             m_state = NEWUSER;             m_connection->Protocol().SendString( *m_connection, yellow +                 "Please enter your desired name: " + reset );         } 

If the user enters "new" as the name, the state of the logon immediately moves to the NEWUSER state, and the user is asked to enter his desired username. If the user enters anything other than "new", this next piece of code is executed:

 else {             PlayerDatabase::iterator itr = PlayerDatabase::findfull( p_data );             if( itr == PlayerDatabase::end() ) {                 m_errors++;                 m_connection->Protocol().SendString( *m_connection,                     red + bold + "Sorry, the user \"" + white + p_data + red +                     "\" does not exist.\r\n" +                     "Please enter your name, or \"new\" if you are new: " +                     reset );             } 

The player database is checked to see if any full names match the name the new user wants. I used a full-name match so that people could have names that partially matched each other. If the name doesn't exist, the user is told that he chose an invalid name, and the connection's error count goes up. If the name already exists, however, the next piece of code is executed:

 else {                 m_state = ENTERPASS;                 m_name = p_data;                 m_pass = itr->Password();                 m_connection->Protocol().SendString( *m_connection,                     green + bold + "Welcome, " + white + p_data + red +                     newline + green + "Please enter your password: " +                     reset );             }         }         return;     } 

The state changes to the state in which an existing user is prompted for his password, and the user's name is recorded into the m_name string. Next, the player's password is retrieved and stored into the m_pass string, and the user is prompted to enter his password.

The previous three code fragments should take care of every possible outcome whenever a new connection makes an entry. Once the segment is complete, the function simply returns, because the user's command has been handled.

The next segment takes care of new users:

 if( m_state == NEWUSER ) {         if( PlayerDatabase::hasfull( p_data ) ) {             m_errors++;             m_connection->Protocol().SendString( *m_connection,                 red + bold + "Sorry, the name \"" + white + p_data + red +                 "\" has already been taken." + newline + yellow +                 "Please enter your desired name: " + reset );         } 

First, the function checks to see if the database has the name that the player wants to use. If the name is already in use, the player is told that the name has already been taken, and the error count is increased again. If the name isn't taken, the function goes on to check if the name is acceptable:

 else {             if( !AcceptibleName( p_data ) ) {                 m_errors++;                 m_connection->Protocol().SendString( *m_connection,                     red + bold + "Sorry, the name \"" + white + p_data + red +                     "\" is unacceptable." + newline + yellow +                     "Please enter your desired name: " + reset );             } 

If the name isn't acceptable, the error count is again increased, and the user is told that the name isn't acceptable. (I haven't covered that function, but it is almost identical to the UserDatabase::IsValidName function found in the SimpleChat demo, Demo 6.2 section in Chapter 6.)

Here is the final segment of code for the NEWUSER state:

 else {                 m_state = ENTERNEWPASS;                 m_name = p_data;                 m_connection->Protocol().SendString( *m_connection,                         green + "Please enter your desired password: " +                         reset );             }         }         return;     } 

At this point, the user has entered a name that is both acceptable and new to the game, so the name is recorded into the m_name string, and the user is asked to enter his desired password. After all of this, the code block returns.

The next block of code handles new password commands:

 if( m_state == ENTERNEWPASS ) {         if( p_data.find_first_of( BasicLib::WHITESPACE ) != string::npos ) {             m_errors++;             m_connection->Protocol().SendString( *m_connection,                     red + bold + "INVALID PASSWORD!" +                     green + "Please enter your desired password: " +                     reset );             return;         } 

The function performs a search on the desired password; if the function finds whitespace, the password is rejected. This has to do with the fact that when passwords are read in by the player database, the database assumes the password won't contain whitespace.

If the password is acceptable, the function continues:

 m_connection->Protocol().SendString( *m_connection,                 green + "Thank you! You are now entering the realm..." +                 newline );         Player p;         p.Name() = m_name;         p.Password() = p_data; 

The user is told that he is now entering the game, and a new Player object is created. The player's name and password are recorded. And this is where the going gets a little tricky. First, before you can insert the user into the database, you need to find him a valid ID. I've decided to use a simple method to calculate the next available ID: find the highest ID in the database, and add 1. Of course, this opens up a whole new can of worms, because the database might be empty.

To solve this problem, I first check to see if the database is empty, and if so, I assume that the first user logging in has an ID of 1 and becomes the administrator of the MUD:

 if( PlayerDatabase::size() == 0 ) {             p.Rank() = ADMIN;             p.ID() = 1;         }         else {             p.ID() = PlayerDatabase::LastID() + 1;         }         PlayerDatabase::AddPlayer( p );         GotoGame( true );         return;     } 

I haven't shown you the PlayerDatabase::LastID function at all, but it's so simple it doesn't really need to be shown. This function retrieves an iterator to the last player in the database and returns its ID.

Once the ID of the player is set, the player is added to the database, and the GotoGame function is invoked with a parameter of true . The parameter signifies that this is a new character, and the handler should set up the next state accordingly . I'll show you what this means when I cover the GotoGame function.

The last block of code for this function checks an existing user's password. Remember that when the player entered an existing username, the function looked up that user's password and stored it in the m_pass string. Here's the code:

 if( m_state == ENTERPASS ) {         if( m_pass == p_data ) {             m_connection->Protocol().SendString( *m_connection,                     green + "Thank you! You are now entering the realm..." +                     newline );             GotoGame();         } 

The code checks to see if the player's entry matches the password of the player he's trying to log in as. If so, the player is told that he's entering, and the GotoGame function is called.

Here's the last part of the code that rejects an incorrect password:

 else {             m_errors++;             m_connection->Protocol().SendString( *m_connection,                     red + bold + "INVALID PASSWORD!" + newline +                     yellow + "Please enter your password: " +                     reset );         }         return;     } } 

That pretty much sums up the logon command handler code.

GotoGame Function

There's only one more piece of code I want to show you before moving on to the training handler. The GotoGame function prepares a connection to connect to the Game handler.

I told you earlier that the function takes a Boolean as a parameter, but it's optional. If you don't pass anything in, the Boolean is assumed to be false . The parameter, when true , indicates that the connection is a "newbie" to the game, and that this is the player's first time connecting. Here's the function:

 void Logon::GotoGame( bool p_newbie ) { {     Player& p = *PlayerDatabase::findfull( m_name );     if( p.LoggedIn() ) {            p.Conn()->Close();         p.Conn()->Handler()->Hungup();         p.Conn()->ClearHandlers();     } 

First, the connection retrieves a reference to the player. (It assumes that the lookup works, considering that any code that calls this function should have already verified that the player exists.) Next, the code checks to see if the player is already logged in. If so, it's probably because the connection died, and the server didn't detect it yet; therefore, the server is told to close the connect and then notify its current handler that the connection hung up. The code continues:

 p.Newbie() = p_newbie;     p.Conn() = m_connection;     p.Conn()->SwitchHandler( new Game( *p.Conn(), p.ID() ) ); } 

The "newbie" state of the player is recorded from the parameter, the new connection is recorded into the player's connection pointer, and the Logon handler is removed and swapped with a new Game handler using the SwitchHandler function. This is a tricky thing to do, though, because when you remove the handler from a connection, it is physically deleted, along with any member variables within the handler. Why is this a concern? Because the function being executed belongs to the handler that you just deleted! Therefore, if you access any member variables from this point on, you crash the program. Because of this, whenever you change states, you need to make sure that the function immediately exits and does not access members .

NOTE

Note that since GotoGame is called by another member function, the functions also need to make sure they exit without accessing mem bers. You should notice that in the Handle function, the function returns after every call to GotoGame .

Finally, the Game handler is set as the connection's new handler, and it's notified about the connection's ID.

Training Handler

The next handler I'm going to cover is the simplest of the three SimpleMUD handlers the handler that allows a player to assign his statpoints to his three core attributes (strength, health, and agility).

Class Skeleton

First, let me show you the class skeleton:

 class Train : public Telnet::handler { public:     void Handle( string p_data );     void Enter();     void Leave();     void Hungup();     void Flooded();     void PrintStats( bool p_clear = true );     Train( Connection<Telnet>& p_conn, player p_player ); protected:     player m_player; }; 

Once again, you can see that the class inherits from the Telnet::handler class, which I showed you back in Chapter 6, and the first four functions in this class are the same handler functions you've seen several times before. The new function (besides the constructor) that this class defines is a helper function. PrintStats prints out a players stats to the player's connection.

The constructor simply takes a reference to a connection, and a player database pointer object, which it records into m_player , so I'm not going to show that function to you.

Leaving and Entering

Like the Logon handler, the Training handler doesn't need to clean up after connections when they leave, so the Leave function is empty:

 void Leave() {}; 

Whenever a connection enters the traning state, the handler needs to perform a little housekeeping. Here's the code:

 void Enter() {     Player& p = *m_player;           // retrieve the Player object     p.Active() = false;              // make the player "inactive"     if( p.Newbie() ) {         p.SendString( magenta + bold +             "Welcome to SimpleMUD, " + p.Name() + "!\r\n" +             "You must train your character with your desired stats,\r\n" +             "before you enter the realm.\r\n\r\n" );         p.Newbie() = false;     }     PrintStats( false ); } 

The function retrieves the actual Player object and makes it inactive. Once that is done, the connection checks to see if the player is a newbie, in which case it prints out a simple welcome message, notifying the user that he needs to assign his 18 statpoints to the various core attributes, and the newbie flag is cleared.

Finally, the function displays the player's stats, using false as the parameter for the PrintStats function. (This means that it shouldn't clear the screen and wipe out the welcome message that was just displayed to the player.)

Closing Connections

Of course, the handler also needs to be able to handle connections that accidentally close (due to flooding, or hanging up). Both the Flooded and Hungup functions call the

 PlayerDatabase::Logout function: void Train::Hungup() {     PlayerDatabase::Logout( m_player ); } void Train::Flooded() {     PlayerDatabase::Logout( m_player ); } 

Handling Training Commands

Four commands are accepted when a character is in the training state: 1 , 2 , 3 , and quit . The three numbers represent three core attributes, with 1 being strength, 2 health, and 3 agility. Whenever a player types quit , the handler removes itself from the connection's handler stack, and the connection should return to the state in which it existed previously (the game state).

 void Train::Handle( string p_data ) {     p_data = BasicLib::LowerCase( ParseWord( p_data, 0 ) );     Player& p = *m_player;               // load the player object     if( p_data == "quit" ) {         PlayerDatabase::Save( p.ID() );  // save player         p.Conn()->RemoveHandler();       // remove the Training handler         // tell the previous handler that it now has control again:         p.Conn()->Handler()->NewConnection( *p.Conn() );         return;     } 

The previous code block is just the first half of the function. The string that the player typed in, p_data , is first lowercased, and then the Player object representing the current player is retrieved. If the player types quit , the player wants to exit the training mode, and go back to the game. Therefore, the newly modified character is saved to disk, the training handler is removed, and the game handler (or whatever handler was below the training handler on the connection's handler stack) is notified that the connection has re-entered that state. Notice how the function immediately returns, since the handler has been changed. I described the reasoning for this with the Logon handler previously.

 char n = p_data[0];     if( n >= '1' && n <= '3' ) {           // make sure number is 1, 2, or 3         if( p.StatPoints() > 0 ) {         // make sure user has points             p.StatPoints()--;              // subtract a point             p.AddToBaseAttr( n - '1', 1 ); // add the point to a base attribute         }     }        PrintStats( true );                    // print stats and clear screen } 

The last half of the code extracts the first letter of the player's command. If it's a valid command, the letter should be 1, 2, or 3. The function checks to make sure that the first letter is 1, 2, or 3, and also makes sure the user has some extra statpoints. If neither of those conditions occurs, the input is ignored.

If the player entered a valid number and has extra statpoints left, the base attribute corresponding to that number is incremented. The line that does this may be a little confusing however, so let me explain it. In the Attribute enumeration I showed you earlier in this chapter, the strength, health, and agility enumerations are given values of 0, 1, and 2. So when the user enters the character 1, and the character 1 is subtracted from that value, you get the actual integer value 0, which is the value of the strength enumeration. Likewise, 2 - 1 yields the actual integer 1, and 3 - 1 yields 2. That's it for this function.

Printing Stats

The final function I want to show you is the PrintStats function:

 void Train::PrintStats( bool p_clear ) {     Player& p = *m_player;           // get player object     if( p_clear ) {         p.SendString( clearscreen ); // clear screen if needed     }     p.SendString( white + bold +     // send stats         "---------------------- Your Stats ----------------------\r\n" +         dim +         "Player:           " + p.Name() + "\r\n" +         "Level:            " + tostring( p.Level() ) + "\r\n" +         "Stat Points Left: " + tostring( p.StatPoints() ) + "\r\n" +         "1) Strength:      " + tostring( p.GetAttr( STRENGTH ) ) + "\r\n" +         "2) Health:        " + tostring( p.GetAttr( HEALTH ) ) + "\r\n" +         "3) Agility:       " + tostring( p.GetAttr( AGILITY ) ) + "\r\n" +         bold +         "--------------------------------------------------------\r\n" +         "Enter 1, 2, or 3 to add a stat point, or \"quit\" to go back: " ); } 

The function retrieves a player from the database, clears the screen if requested to do so, and then prints out the player's statistics, consisting of name, level, stat points remaining, and the three core attributes.

Game Handler

The third and final handler, the Game handler, is the largest and most complex handler in the game. This is the handler that reacts to every command that the user types.

The previous two handlersthe logon handler and the training handlerare complete. They don't need features added later on in the game. The Game handler, however, will obviously be incomplete when this chapter is finished. I haven't defined enemies, shops , or the entire map system yet, so things like combat, commerce, and movement cannot be implemented.

Instead, the current incarnation of this handler implements all the features of the game that don't require rooms, enemies, or shops.

Here is a list of all the user commands that will be implemented in this version of the game handler:

 chat experience help inventory quit remove stats time use whisper who kick announce changerank reload items shutdown 

If you'll remember back from Chapter 7, everything above kick is available to everyone, everything above announce is available to gods and higher, and every command is available to admins.

Game Data

Several pieces of data are associated to the game handlers. (I've removed the functions.)

 class Game : public Telnet::handler { protected:     player m_player;     string m_lastcommand;     static BasicLib::Timer s_timer;     static bool s_running; }; 

The handler keeps a player database pointer so that it can look up a player's data whenever it needs to. It also keeps a string , which tracks the last command the user entered. It's usually a good "user interface" issue to allow a player to repeat his last command, so this helps.

The handler also has two static variables, which means they act like global variables inside of the Game class. The s_timer keeps track of how long the game has been running, and the s_running Boolean keeps track of whether the game is actually running. Setting the Boolean to false tells the game that it has been shut down, and causes the program to exit.

Game Functions

The Game handler has quite a few functions dealing with all sorts of stuff, so I'm going to list them in related categories. The first category is, of course, the standard handler functions:

 void Handle( string p_data ); void Enter(); void Leave(); void Hungup(); void Flooded(); 

You've seen them all a few times before, so there's really no need to explain their purposes yet again. The next group of functions sends text to different groups of players:

 static void SendGlobal( const string& p_str ); static void SendGame( const string& p_str ); static void Announce( const string& p_announcement ); static void LogoutMessage( const string& p_reason ); void Whisper( string p_str, string p_player ); 

Four out of the five functions are static, which means that they can be called within any part of the game without needing a Game object. SendGlobal is a function that sends a single string to any player who is logged on, no matter what state he is in.

NOTE

When a connection is still in the Logon state, it hasn't actually logged on to a player yet, so the SendGlobal function does not send text to connections within that state.

The SendGame function is similar, but instead of sending a string to every player who is logged in, it limits the scope a little and sends a string to every player who is active within the game. Announce and LogoutMessage are simple wrappers around SendGlobal and SendGame . They attach standard coloring schemes and text to game announcements and logoff announcements, so that the game keeps a consistent look and feel.

Finally, the Whisper function attempts to "whisper" some text from the current player to a player named within the p_player parameter string.

The next functions deal with generating informational strings:

 static string WhoList( const string& p_who ); static string PrintHelp( PlayerRank p_rank = REGULAR ); string PrintStats(); string PrintExperience(); string PrintInventory(); 

The first two strings are static, since they require no specific player information to be generated. The who-list is a listing of everyone in the game, and the help-list generates a list of all the functions available to any person who has been given a rank.

The stats, experience, and inventory listings all depend on a single player in the game, so those obviously cannot be static.

And finally, here's the rest of the functions:

 bool UseItem( const string& p_item ); bool RemoveItem( string p_item ); inline static BasicLib::Timer& GetTimer()       { return s_timer; } inline static bool& Running()                   { return s_running; } Game( Connection<Telnet>& p_conn, player p_player ); inline static void Logout( player p_player ); void GotoTrain(); 

Those are functions to use an item ("arm" an item), remove an item ("disarm"), get the timer object, get the running Boolean, construct the game handler, log a player out (this is a helper function), and move a player into the training state (another helper function).

Handler Functions

Three out of the four handler functions are pretty simple; the only exception is the Handle function, which is pretty large. I'll cover the simple handler functions first.

New Connections

If you'll recall the Logon handler, whenever a player successfully logs on in that handler, it switches the state of the connection to the Game handler. When that happens, this function is called:

 void Game::Enter( ) {     USERLOG.Log(  GetIPString( p_connection.GetRemoteAddress() ) +                   " - User " + m_player->Name() +                   " entering Game state." );     m_lastcommand = "";     Player& p = *m_player;     p.Active() = true;     p.LoggedIn() = true;     SendGame( bold + green + p.Name() + " has entered the realm." );     if( p.Newbie() )         GotoTrain(); } 

The function is fairly straightforward. The player log is updated to show that a player entered the game state, the connection is recorded, and the last command string is cleared.

You should remember the "newbie" status from the Logon handler section of this chapter. If a player logs in for the first time, that player has his "newbie" flag set, but it isn't set for a pre-existing player who logs in. If a player is a newbie, he hasn't changed his stats and must be taken to the Train handler. New players also have their room ID set to 1, which is the "main room" of the game.

If the player isn't a newbie, he's activated, and everyone in the game is told that he's joined.

Leaving the Handler

Of course, whenever the player leaves the game state, he needs to tell the game about it, so that's what the Leave function takes care of:

 void Game::Leave() {     m_player->Active() = false;     if( m_connection->Closed() )         PlayerDatabase::Logout( m_player ); } 

This code deactivates the player, and checks to see if the connection has been closed. If it has been, the player database is told to log the player out. Otherwise, it is assumed that the player is still logged into the game (probably switching to the training state), and you don't want to log him off if that happens. You should log a player off when the connection closes .

Closed Connections

Whenever a connection is unexpectedly closed, the game handler needs to take care of this occurrence:

 void Game::Hungup() {     Player& p = *m_player;     LogoutMessage( p.Name() + " has suddenly disappeared from the realm." ); } void Game::Flooded() {     Player& p = *m_player;     LogoutMessage( p.Name() + " has been kicked out for flooding!" ); } 

Instead of just logging players off as the training handler does, the players within the game handler notify everyone else within the game when they log out. Both of these functions utilize the LogoutMessage helper function to notify everyone.

Handling Commands

By far, the largest function within the game handler is the Handle command. In a more complex MUD, this kind of function would be more segmented, but this MUD is simple enough so that's not necessary.

I'm splitting up the function so that I can show you each command in detail, starting with the repeating command:

 void Game::Handle( string p_data ) {     Player& p = *m_player;     if( p_data == "/" ) {         p_data = m_lastcommand;     }     else {         m_lastcommand = p_data;     }     string firstword = BasicLib::LowerCase( ParseWord( p_data, 0 ) ); 

As usual, p_data is the string that contains the command that the player typed in.

The command to repeat the player's last command is simply a slash (/). If the command is a slash, p_data is reassigned with the value of the m_lastcommand string. If the user typed in anything other than a slash, the m_lastcommand is updated with the value of p_data .

The last part of the code fragment strips out the first word the player typed, makes the word lowercased, and then stores it in a local variable named firstword . Now the function starts trying to find out what command the player typed in:

 if( firstword == "chat"  firstword == ":" ) {         string text = RemoveWord( p_data, 0 );         SendGame( white + bold + p.Name() + " chats: " + text );         return;     } 

I've added a shortcut for players to use when they chat. Instead of being required to type "chat hello everyone!", players can type a single colon instead of the whole word "chat". For example, a player would type ": hello everyone!" instead. The function removes the command ("chat" or ":") from the string and stores the rest of the string in text , and then sends that text out to everyone who is active in the game, in the form of "Ron chats: hello every-one!". Since the command was successfully handled, the function returns; there is no need to continue checking to see if the command needs to be handled.

The next four commands simply display status reports for the current player:

 if( firstword == "experience"  firstword == "exp" ) {         p.SendString( PrintExperience() );         return;     }     if( firstword == "help"  firstword == "commands" ) {         p.SendString( PrintHelp( p.Rank() ) );         return;     }     if( firstword == "inventory"  firstword == "i" ) {         p.SendString( PrintInventory() );         return;     }     if( firstword == "stats"  firstword == "st" ) {         p.SendString( PrintStats() );         return;     } 

I'm pretty sure those commands are self-explanatory, so we'll move on:

 if( firstword == "quit" ) {         m_connection->Close();         LogoutMessage( p.Name() + " has left the realm." );         Logout( p.ID() );         return;     } 

Closing connections is always a tricky business, simply because it's so difficult to keep track of connections if they suddenly disappear. Remember that the Connection class simply sets a Boolean to true whenever you close a connection, so that a ConnectionManager can later check and close the connection when it does its housekeeping.

The realm is told that the player left, and the player is logged out, so there's nothing more for this function to do.

Here are the two item functions:

 if( firstword == "remove" ) {         RemoveItem( ParseWord( p_data, 1 ) );         return;     }     if( firstword == "use" ) {         UseItem( RemoveWord( p_data, 0 ) );         return;     } 

The syntax for the remove command is relatively simple. You don't tell the game the name of the item you want to disarm; instead, you tell the game remove armor or remove weapon . That makes life simpler, really. That's why the function parses out word 1 (remember, remove is word 0), and passes that into the RemoveItem helper function.

When a player uses an item, however, you need to type the name of the item you want to use, such as use giant sword or whatever. That's why the use command strips out the word remove and passes the rest of your string into the UseItem helper.

While not completely necessary for the game, I always find it extremely useful to have a time function:

 if( firstword == "time" ) {         p.SendString( bold + cyan +                       "The current system time is: " + BasicLib::TimeStamp() +                       " on " + BasicLib::DateStamp() +                       "\r\nThe system has been up for: "                       + s_timer.GetString() + "." );         return;     } 

This function displays the current system time, and then it shows how long the server has been running. I like to have functions like this so I can tell how long the server has been running. Admit it, as a nerd, it's always fun to brag about how long your system has been online. Linux geeks especially like to brag about this stuff, and frequently use this information to win arguments with Windows nerds. As much as I love the newest versions of Windows, you have to admit, Linux stays up and running much longer.

Next up is the "whisper" command, which allows a player to privately message another person in the game, without everyone else hearing what he has to say. This can help when a player is planning some nefarious scheme or another; here's the code:

 if( firstword == "whisper" ) {         string name = ParseWord( p_data, 1 );         string message = RemoveWord( RemoveWord( p_data, 0 ), 0 );         Whisper( message, name );         return;     } 

You use the command like this: "whisper ron Hello there!" The function strips out word 1 ("ron"), which is the name of the player you are whispering to, and then it strips off the first two words ("whisper ron") and uses the rest of the string ("Hello there!") as the message. Finally, it calls the Whisper helper function.

The final regular-user command is the "who" command:

 if( firstword == "who" ) {         p.SendString( WhoList( BasicLib::LowerCase(             ParseWord( p_data, 1 ) ) ) );         return;     } 

This function follows a process similar to other commands, by stripping off word 1. Then it lowercases the second word, and sends it off to the WhoList function, which displays a list of people in the realm to the player.

There's a reason why this command has a parameter: You can choose which people are included in the list. By default, if there is no parameter, the result of WhoList is just the return of a list of everyone who is currently logged in. If you use the parameter of all , however, such as who all , the function returns a list of everyone in the entire game, even those who aren't logged in.

Now, here is the "kick" god-command, which physically kicks people out of the game:

 if( firstword == "kick" && p.Rank() >= GOD ) {         PlayerDatabase::iterator itr =             PlayerDatabase::findloggedin( ParseWord( p_data, 1 ) );         if( itr == PlayerDatabase::end() ) {             p.SendString( red + bold + "Player could not be found." );             return;         }         if( itr->Rank() > p.Rank() ) {             p.SendString( red + bold + "You can't kick that player!" );             return;         }         itr->Conn()->Close();         LogoutMessage( itr->Name() + " has been kicked by " +                         p.Name() + "!!!" );         PlayerDatabase::Logout( itr->ID() );         return;     } 

The function first searches for someone who is also logged in to kick. You can't kick people who aren't logged in, of course. If no person is found, the kicker is informed.

If a person is found, the game compares ranks; a person can only kick a person whose rank is lower. In SimpleMUD, this means that god s cannot kick admin s, since the rank of admin s is higher. It's "chain of command" type stuff.

Finally, the connection for the kickee is closed, the realm is notified that the player was kicked out, and the database is also told about it.

The last four commands are administrator-only commands, meaning that only people with a rank of ADMIN can execute them. The "announce" command sends an announcement to everyone in the game (even people in the Train handler):

 if( firstword == "announce" && p.Rank() >= ADMIN ) {         Announce( RemoveWord( p_data, 0 ) );         return;     } 

This simply removes the first word "announce" from the string, and sends it off to the Announce helper function.

Here is the command to change a player's rank:

 if( firstword == "changerank" && p.Rank() >= ADMIN ) {         string name = ParseWord( p_data, 1 );         PlayerDatabase::iterator itr = PlayerDatabase::find( name );         if( itr == PlayerDatabase::end() ) {             p.SendString( red + bold + "Error: Could not find user " +                           name );             return;         }         PlayerRank rank = GetRank( ParseWord( p_data, 2 ) );         itr->Rank() = rank;         SendGame( green + bold + itr->Name() +                       "'s rank has been changed to: " +                   GetRankString( rank ) );         return;     } 

This function finds a player with the name you requested and changes his rank; the player doesn't even have to be online. Everyone in the game is made aware of the rank changing as well.

The next command allows you to reload the item database:

 if( firstword == "reload" && p.Rank() >= ADMIN ) {         string db = BasicLib::LowerCase( ParseWord( p_data, 1 ) );         if( db == "items" ) {             ItemDatabase::Load();             p.SendString( bold + cyan + "Item Database Reloaded!" );         } 

If the user wants the item database to be reloaded, he needs to type in "reload items", and this immediately causes the item database to be reloaded. Reloading the player database isn't currently possible with the version of the MUD described in this chapter, but this functionality will be added in the next chapter.

The last command allows an administrator to remotely shut the server down. Because of this command, it is wise to entrust administrator access only to responsible people, so that they don't end up shutting down the MUD as a prank:

 if( firstword == "shutdown" && p.Rank() >= ADMIN ) {         Announce( "SYSTEM IS SHUTTING DOWN" );         Game::Running() = false;         return;     } 

All that needs to be done is setting the Game::s_running Boolean to false (through the Running() accessor function), and the main game loop detects that setting and shuts the game down.

And finally, if the game doesn't recognize your command, it sends the text as a chat message:

 SendGame( bold + p.Name() + " chats: " + p_data ); } 

This line of code only exists for the time being. In the next chapter, when I develop the map system, all invalid commands are interpreted as "talking to everyone in the current room". Obviously, since there's no map system yet, I can't have that functionality.

Sending Functions

As you have seen before, five different "sending" functions are defined within the Game class. They are all pretty simple, so I'm not going to launch into a huge lecture about them; rather, I'll go over them somewhat quickly.

Sending to the Game and Sending Globally

The functions SendGame and SendGlobal send strings to every connection that is active or logged in, respectively. For example:

 void Game::SendGlobal( const string& p_str ) {     operate_on_if( PlayerDatabase::begin(),                    PlayerDatabase::end(),                    playersend( p_str ),                    playerloggedin() ); } 

This calls my special operate_on_if algorithm from the BasicLib . Essentially , operate_on_ if acts like the std::for_each algorithm, except that instead of using the playersend functor on every value in the collection, it applies playersend only to players who pass the playerloggedin testing functor. (I showed this functor to you when I was showing you the Player class.) Essentially what this means is that it loops through every player in the database and sends the string to everyone who is logged on.

The SendGame function is virtually identical; the only difference is that instead of the playerloggedin functor, it uses the playeractive functor to send stuff only to active players, not to inactive players.

Helpers

There are two helper functions that help send strings; I've mentioned them before:

 void Game::LogoutMessage( const string& p_reason ) {     SendGame( SocketLib::red + SocketLib::bold + p_reason ); } void Game::Announce( const string& p_announcement ) {     SendGlobal( SocketLib::cyan + SocketLib::bold +                 "System Announcement: " + p_announcement ); } 

As I've said before, these functions exist to provide a certain look and feel to the game, because many different places within the code may be making announcements or saying that someone has logged off, and you want these messages to look consistent throughout the game.

Whispering

Whispering from one person to another requires a little bit more work than the other communication methods . First, the game needs to find the person you're whispering to, and then tell that person what you said, as well as telling yourself what you said to that player:

 void Game::Whisper( std::string p_str, std::string p_player ) {     PlayerDatabase::iterator itr = PlayerDatabase::findactive( p_player );     if( itr == PlayerDatabase::end() ) {         m_player->SendString( red + bold + "Error, cannot find user." );     }     else {         itr->SendString( yellow + m_player->Name() + " whispers to you: " +                          reset + p_str );         m_player->SendString( yellow + "You whisper to " + itr->Name() +                               ": " + reset + p_str );     } } 

If the player isn't found, an error string is printed. But if the player is found, the message is sent to both the player and yourself, albeit in slightly different forms for each. If you type whisper bob hello! , he'll see Ron whispers to you: hello! , and you'll see You whisper to bob: hello! .

Status Printers

I am really running short on space about now, and I'm sure you're getting tired of seeing all this code as well. Therefore, I'll skip showing you the status printing code, since it is essentially just a big mess of formatted text-printing functions.

There is one note I'd like to make however, which is about the WhoList printer. To get a configurable function that would optionally print only players who are online, or all players in the database, I decided to use the operate_on_if algorithm and create a wholist functor (notice the lack of capitals), which prints out the "wholist entry" line for a single player.

Here's part of the code for the WhoList function:

 wholist who; who = BasicLib::operate_on_if(                 PlayerDatabase::begin(),                 PlayerDatabase::end(),                 wholist(),                 playerloggedin() ); 

The operate_on_if algorithm returns the "operation functor" that you passed into it, because in this case, the wholist functor keeps track of a string of who-list entries. Every time wholist finds a player who is logged in, it creates an entry for that player and adds that string to the end of its str member variable, so when the function returns, you can get a string representing every entry in the list generated from the function. As you can see, the operation of the operate_on_if algorithm combined the wholist functor on a collection of players. The entry of every player who is online is calculated and added to the wholist 's str , which is a string. Of course, the entries consist of more than just the player's name, but for simplicity's sake, that's all I show here.

Figure 8.10 shows a loose representation of what occurs.

Figure 8.10. Every time wholist finds a player who is logged in, it creates an entry for that player and adds that string to the end of its str member variable.

graphic/08fig10.gif


Figure 8.11 shows a sample who-listing screenshot.

Figure 8.11. In this sample who-listing, each entry consists of a player's name, level, online activity, and ranking.

graphic/08fig11.gif


Item Functions

The final functions I'm going to cover here are the two item functions, which arm/use or disarm items in a player's inventory.

Using an Item

You can use the use command on all three types of items, and the behavior of the function differs depending on the type. Let me show you the code first:

 bool Game::UseItem( const std::string& p_item ) {     Player& p = *m_player;     int i = p.GetItemIndex( p_item );     if( i == -1 ) {         p.SendString( red + bold + "Could not find that item!" );         return false;     } 

The previous code segment attempts to retrieve the index of the item the player is requesting for use. If none is found, the function tells the player so and returns.

 Item& itm = *p.GetItem( i );     switch( itm.Type() ) {     case WEAPON:         p.UseWeapon( i );         return true;     case ARMOR:         p.UseArmor( i );         return true; 

Once the item is found, it is retrieved from the item database, and the function performs a switch on the type of the item. Armor and weapons are similar; they each call the player's UseWeapon or UseArmor functions. Nothing is printed out to the user at this time; this functionality is implemented in the next chapter.

 case HEALING:         p.AddBonuses( itm.ID() );         p.AddHitpoints( BasicLib::RandomInt( itm.Min(), itm.Max() ) );         p.DropItem( i );         return true;     }     return false; } 

In the case of a healing item, however, the bonuses of that item are added to the player's stats, the hitpoints that the item heals are calculated using the BasicLib::RandomInt function and added to the player, and then the function calls the Player::DropItem on the item you just used. If you remember, the DropItem removes an item from a player's inventory. Once you use a healing item, it simply disappears, to prevent you from using it over and over again.

Disarming an Item

And finally, a player can disarm his weapon or his armor:

 bool Game::RemoveItem( std::string p_item ) {     Player& p = *m_player;     p_item = BasicLib::LowerCase( p_item );     if( p_item == "weapon" && p.Weapon() != 0 ) {         p.RemoveWeapon();         return true;     }     if( p_item == "armor" && p.Armor() != 0 ) {         p.RemoveArmor();         return true;     }     p.SendString( red + bold + "Could not Remove item!" );     return false; } 

Depending on whether the player types weapon or armor , the player's weapon or armor is removed, but only if the player has a weapon or piece of armor that is armed in the first place. If a player has nothing armed, an error is printed to the player.

Helpers

There is one helper function, whose main purpose is to put a player into the training state:

 void Game::GotoTrain() {     Player& p = *m_player;     p.Active() = false;     p.Conn()->AddHandler( new Train( p.ID() ) );     LogoutMessage( p.Name() + " leaves to edit stats" ); } 

It's a helpful function to use whenever a player needs to edit his stats.

[ LiB ]


MUD Game Programming
MUD Game Programming (Premier Press Game Development)
ISBN: 1592000908
EAN: 2147483647
Year: 2003
Pages: 147
Authors: Ron Penton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net