The Meat of the Game

[ LiB ]

The final area that the game module involves is controlling the physical side of the game. It moves things around, deletes them, and tells everyone about the events that happened in the game.

In the previous chapter, I showed you all the actions, and in Chapters 12 and 13, I showed you the entity classes and what you can do with them. Now, I can finally show you how to move things around.

Considerations

The Game.cpp file is by far the largest file in the BetterMUD's C++ core , the only one surpassing 1,000 lines of code.

The reason for this is that there are quite a few physical actions that must be taken care of, and every one of them needs to follow a rigorous process.

In retrospect, I wish I could have designed an even more flexible system of entities, in which the type of the entity is variable. This could become quite useful later on if you ever need to transmogrify items from one type into another, like people turning into stone items, or the other way around. Now, the scripting engine can simulate things like that, but a more flexible core system could have that ability built-in. Another benefit of a flexible system like that is less code clutter.

For the BetterMUD, you'll see that there are three functions for moving items around: player to player, player to room, and room to player. The problem is that these functions all have the same basic purposeto transfer the ownership of an entity from one entity to another. In a system that treated all entities in the same way, you could conceivably turn these three functions into a single function. I want you to be thinking about these ideas for the future; the BetterMUD isn't perfect, but it's much better than the SimpleMUD.

Transaction Processes

Every physical transaction in the game has a certain general process to follow when it is executed. I've mentioned this before, but let me restate the full process so it's easier to see:

  1. Retrieve actor entities from databases.

  2. Perform an integrity check.

  3. Ask permission to perform the transaction.

  4. Perform the physical movement of the actors.

  5. Notify every actor involved that the transaction occurred.

  6. Clean up (optional).

Sample Transaction Process

Let me begin by showing you the transaction process of the "attempttransport" game action, which is simpler than most others, since it doesn't involve portals.

Retrieving the Actors

Here is the process through which the game retrieves the actors:

  1. Retrieve the character who is transporting.

  2. Retrieve the room the character is leaving.

  3. Retrieve the room the character is entering.

  4. Retrieve the region the character is leaving.

  5. Retrieve the region the character is entering (may be the same as 4).

  6. Figure out if the character is changing regions .

This simple process involves looking up five distinct entities: a character, two rooms, and one or two regions, depending on if the character is switching regions or not. If any of these database lookups fail, they throw an exception. Obviously if you can't look them up, something really bad has happened, because one of the entities in the equation just doesn't exist. At this point, you should simply give up.

NOTE

The code is part of the Transport helper function, which is not callable outside the Game class. I'll show you a complete listing of the helper functions after this example.

There's absolutely no way you can possibly try to move the character if any of the actors are missing in action. So the action throws an exception, and whatever is calling the action catches the exception and cleans up the action.

Here's the accompanying code:

 void Game::Transport(         entityid p_character,                   // character who left         entityid p_room )                       // room id {     character c( p_character );     room oldroom( c.Room() );     room newroom( p_room );     region oldreg( oldroom.Region() );     region newreg( newroom.Region() );     bool changeregion = oldroom.Region() != newroom.Region(); 

Nothing here is substantially new; all the databases are consulted for their entities, and the region database may be consulted twice, but I'm not terribly concerned about that since I know it's a vector database with almost instant lookups.

Integrity Checking

The transport transaction doesn't need to do any integrity checking, since it is assumed that you can transport any character from one room to another. You'll see integrity checking later on when I show you item movement; those transactions require that the actors be within the same room.

Asking Permission

The next part of the process asks permission of everyone involved to see if the character can be transported. Here's the exact process:

  1. Ask the region in which the character is located if he can leave.

  2. Ask the new region if the character can enter.

  3. Ask the character if he can leave the region he's in.

  4. Ask the character if he can enter the new region.

  5. Ask the room in which the character is located if the character can leave.

  6. Ask in the new room if the character can enter.

  7. Ask the character if he can leave the room he's in.

  8. Ask the character if he can enter the new room.

Most of the time the character isn't switching regions, so the first four queries aren't executed, and only 5 through 8 are queried. That's still a high number of different permissions to ask, however.

If any of the queries replies negatively, the entire action is abandoned , and the function returns. This means that any one of the actors involved can deny the movement of a character from one room to another. An Elven region may have a magical spell cast on it that doesn't allow Dwarves into the region at all; or perhaps a character is trapped in a special region that won't allow him to be transported out. Maybe your character has a condition that won't allow him to walk. Whatever the reason, the key is that any of the actors involved can deny a character the ability to enter or leave.

NOTE

Because of the fact that any entity can deny a built-in action, you should try to keep your denial scripts sparse. If you have too many denial scripts, your players might encoun ter some unexpected behavior when they try moving from one place to another, yet your scripts just don't allow it, even if they technically should be able to move.

Here's the code to ask permissions:

 if( changeregion ) {     if( oldreg.DoAction( "canleaveregion", p_character, oldreg.ID() ) == 1 )         return;     if( newreg.DoAction( "canenterregion", p_character, newreg.ID() ) == 1 )         return;     if( c.DoAction( "canleaveregion", p_character, oldreg.ID() ) == 1 )         return;     if( c.DoAction( "canenterregion", p_character, newreg.ID() ) == 1 )         return; } if( oldroom.DoAction( "canleaveroom", p_character, oldroom.ID() ) == 1 )     return; if( newroom.DoAction( "canenterroom", p_character, newroom.ID() ) == 1 )     return; if( c.DoAction( "canleaveroom", p_character, oldroom.ID() ) == 1 )     return; if( c.DoAction( "canenterroom", p_character, newroom.ID() ) == 1 )     return; 

In the previous chapter, I told you about logic collections, and how they contain 0 or more logic modules. When you type something like c.DoAction( ) , you're telling the character to send an action to its logic collection.

The collection then tries sending the action to every logic module it contains, but if any of them returns a non-zero value, it stops executing them and returns the result right away. Almost all the query actions must return one of two values:

0Operation is allowed

1Operation is denied

I say almost, because there is one query that does just the opposite : the custom "query" action.

The physical part of the game is like a country's constitution; it defines all the rights of entities in the game. By default, entities can do anything the game core defines; characters can get, drop, and give items; characters can move around, say things, and so on.

NOTE

The built-in queries are designed to be called only when a character is serious about entering. The query assumes that the character tries to perform the action. In other words, the query assumes that the character does not ask to perform an action and then decide not to perform it. Because of this, when an actor denies an action, such as a force field denying a player entrance to a room, the force-field script must give the player an error message. This is an important element in making your game more variable. Instead of making a command that tries to move a character from room to room and print generic statements such as, You cannot enter this portal , you allow the force field to give a specific message, such as, "You are burned badly by walking into the force field!" Then you take off a few hitpoints if you like.

These rights must be specifically taken away in specific instances. To understand this, think of the fact that shouting "fire" in a crowded theatre is illegal, and yet we still have freedom of speech in the USA.

On the other hand, you can define new actions in the game, but characters don't have the right to perform those actions by default. For example, characters don't have the right to kill another person; they must have scripts that specifically allow them to kill. You'll see how this works in more detail in Chapter 18.

So whenever a character's built-in action queries return 1, that means he must have the right to perform that action removed, and the function should return without doing anything.

Physical Movement

Now you're ready to physically move the character from one room to another. This is a very easy process:

  1. Remove the character from the region he's in.

  2. Set a new region for the character.

  3. Add the character to the new region.

  4. Remove the character from the former room.

  5. Set the new room of the character.

  6. Add the character to the new room.

The process simply uses the modifier functions you can find within the entity classes you learned about in Chapters 12 and 13:

 if( changeregion ) {     oldreg.DelCharacter( p_character );     c.SetRegion( newreg.ID() );     newreg.AddCharacter( p_character ); } oldroom.DelCharacter( p_character ); c.SetRoom( newroom.ID() ); newroom.AddCharacter( p_character ); 

At this point, the objects have been physically moved, and no matter what happens from now on, the actual transaction has been completed.

Notifications

Of course, there's still more work. Now that you've physically moved the character, you must inform everyone involved:

  1. Tell the region that the character left.

  2. Tell the character that he left the region.

  3. Tell the room that the character left.

  4. Tell the character that he left the room.

  5. Tell all characters in the room that the character left.

  6. Tell all items in the room that the character left.

  7. Tell the new region that the character entered.

  8. Tell the character that he entered the new region.

  9. Tell the new room that the character has entered.

  10. Tell all characters in the new room that the character has entered.

  11. Tell all items in the new room that the character has entered.

You should note several items in this listing. The first is that notifications 1, 2, 7, and 8 are optional, and occur only when the user is changing regions. Although you could conceivably want regions to know whenever someone moves between rooms inside the same region, I honestly couldn't find much justification for it.

You may think there's a step missing. "But you never told the character that he entered the new room!" Actually, I did, but it's very subtle. In step 10, when I tell all the characters in the new room that someone entered, that includes the character that was just transported. This is because when the code is executed, the character has already been moved into the new room.

Likewise, when a room the character was in is told about the character leaving, it must assume that the character has already left, and shouldn't go looking for him.

Here's the code to accomplish this:

 if( changeregion ) {     oldreg.DoAction( "leaveregion", p_character, oldreg.ID() );     c.DoAction( "leaveregion", p_character, oldreg.ID() ); } oldroom.DoAction( "leaveroom", p_character, 0 ); c.DoAction( "leaveroom", p_character, 0 ); ActionRoomCharacters( Action( "leaveroom", p_character, 0 ), c.Room() ); ActionRoomItems( Action( "leaveroom", p_character, 0 ), c.Room() ); if( changeregion ) {     newreg.DoAction( "enterregion", p_character, newreg.ID() );     c.DoAction( "enterregion", p_character, newreg.ID() ); } newroom.DoAction( "enterroom", p_character, 0 ); ActionRoomCharacters( Action( "enterroom", p_character, 0 ), c.Room() ); ActionRoomItems( Action( "enterroom", p_character, 0 ), c.Room() ); 

The lines roughly correspond to the steps I previously outlined. The only special part of this code is the call to ActionRoomCharacters and ActionRoomItems . These are two helper functions that automatically loop through every character or item in a given room, and send them an action. They're simple loops , so I'm sure you'd be bored out of your mind if I showed them to you; I made them helper functions simply because I need to call them often, and I'd prefer a one-line function call over pasting the same 11-line loop code in multiple places.

Cleanup

I mentioned before that the final step in a transaction is cleanup, but there is no cleanup step for transports. Everything that's needed to be done has already been done. The only transactions that need cleanups are the item-movement actions, and I'll show you an example of that later on.

Analysis

The code for the previous transaction is fairly lengthy, but it follows a well-defined process, so it's not that difficult to follow. The only downside is that you need to remember which actions send which notifications, and from which actors the actions need to ask permissions. Otherwise , the code is fairly straightforward.

Earlier in this section, I discussed a more flexible system in which you could simply define an ownership hierarchy between entities of arbitrary types. Instead of saying explicitly "characters can own items, rooms can own characters, regions can own rooms," and so forth, you might want to simply define the concept of general ownership. I mentioned the fact that you could simplify the concepts of "give," "get," and "drop" into one concept: "transfer item from a to b."

Why not take it one step further? Isn't moving a character from one room to the next similar to moving an item from a player to the ground? Couldn't you simply define one function, in which an entity type would automatically know what kinds of entities it notifies, and then you could simply code one function to move an entity's ownership from one place to another? That way, you could end up with just one function to handle characters moving around, items moving around, or even rooms and portals being moved arounda function that the BetterMUD doesn't support. Just some things to ponder when you go off and make TheBestInTheWorldMUD.

Item Transaction Example

I won't show you all the transactions because much of the information would be redundant, but I do want to show you another example of a transactionan item transaction. This transaction moves an item from a room into a player's inventory.

Database Lookups

Here's the part of the code for an item transaction that performs the database lookups:

 void Game::GetItem(     entityid p_character,                   // character who wants item     entityid p_item,                        // item     entityid p_quantity )                   // optional quantity {     character c( p_character );     item i( p_item );     room r( c.Room() );     region reg( r.Region() ); 

Nothing special here, the character, room, item, and region are all retrieved.

Integrity Checking

This step wasn't needed for transporting a player from one place to another, but it is required for getting an item.

For a character to gain ownership over an item, the item must be within the same room. Trying to get an item that is in a different room from the character doesn't really make any physical sense, so we need to check for that first.

In addition, we also need to check to make sure that the character is trying to pick up a valid quantity of a quantity object. This ensures that a character doesn't get 100 gold coins, when there are only 20 in the room, leaving the game to think that the room now has -80 coins . While that may be humorous to some, it's obviously something that you don't want happening anywhere in the game.

Here's the code:

 if(i.Room() != c.Room()  i.Region() == 0)     throw Exception(         "Character "+c.Name()+"tried picking up item "+i.Name() +         "but they are not in the same room."); if(i.IsQuantity() && p_quantity < 1) {     c.DoAction( "error", 0, 0, 0, 0,         "You can't get "+BasicLib::tostring(p_quantity) +         "of those, it's just not physically possible! FOOL!");     return; } if(i.IsQuantity() && p_quantity > i.GetQuantity()) {     c.DoAction( "error", 0, 0, 0, 0,         "You can't get "+BasicLib::tostring(p_quantity) +         ", there are only "+BasicLib::tostring(i.GetQuantity())+"!");     return; } 

Usually the command that is making a player pick up items must ensure that the player gets the appropriate quantity, but you can never assume that the scripts will be playing nicely with the game. Someone, through error or malicious intent, may make a script that tries to get too many or too little of a quantity object (such as a negative number), so when this happens, you need to tell the offending character that an error has occurred.

Asking Permission

Asking permission for item movement is a simple process:

 if(i.DoAction( "cangetitem", p_character, p_item, p_quantity) == 1)     return; if( r.DoAction( "cangetitem", p_character, p_item, p_quantity ) == 1 )     return; if( reg.DoAction( "cangetitem", p_character, p_item, p_quantity ) == 1 )     return; if( c.DoAction( "cangetitem", p_character, p_item, p_quantity ) == 1 )     return; 

This code simply asks the item, the room, the region, and the character if the item can be retrieved. Any one of these actors can deny it, and this makes your game pretty flexible.

Physical Movement

The physical movement of item entities is a tad more complex than the movement of characters, because items can be " quantities ." One object could be a "pile of 10 coins," of which a player may only want to grab five, and let his buddy get the other five.

In this case, you're going to have to spawn a completely new item to represent one pile of five, and subtract five from the original pile. Here's the first part of the code that handles quantity objects:

 entityid newitemid = 0; if( i.IsQuantity() && p_quantity != i.GetQuantity() ) {     newitemid = ItemDB.generate( i.TemplateID() );      // generate new item     item( newitemid ).SetQuantity( p_quantity );        // set quantity     i.SetQuantity( i.GetQuantity() - p_quantity );      // reset old quantity } 

This checks to see if the item is a quantity object, and if so, checks to see if the desired quantity is different from the existing quantity.

If the quantities are the same (meaning the player wants all ten coins), you don't have a problem, and you can simply move the item as normal.

On the other hand, you'll need to generate a brand new object to represent partial quanti-ties, which is precisely what this part of the function does.

A new item is spawned, and its quantity is set to the requested quantity. At the same time, the previous item has its quantity reduced by the requested amount.

On the other hand, if you're transferring a normal object, or a whole quantity object, this code is executed instead:

 else {     r.DelItem( p_item );     reg.DelItem( p_item );     newitemid = i.ID(); } 

This simply deletes the item from the room and region, and sets newitemid to the ID of the item being transferred.

The final physical act is now processed :

 item newitem( newitemid ); newitem.SetRoom( c.ID() ); newitem.SetRegion( 0 ); c.AddItem( newitem.ID() ); 

The newitem accessor now points to the item that is being moved (either the normal item, or a new quantity item), sets its room and its ID, and then adds it to the character.

Notifications

Next up is the notification code, whereby everyone is told about the movement:

 r.DoAction( "getitem", p_character, newitemid, p_quantity ); newitem.DoAction( "getitem", p_character, newitemid, p_quantity ); ActionRoomCharacters( Action( "getitem", p_character, newitemid,                               p_quantity ), c.Room() ); ActionRoomItems( Action( "getitem", p_character, newitemid,                          p_quantity ), c.Room() ); 

The room is told an item left, the item is told that it left, all the characters in the room are told that the item was retrieved, and so are all the items.

Cleanup

Unlike moving characters, getting items has a cleanup phase. This is needed whenever you move a quantity object onto a player; the game needs to see if you have any duplicate quantity items (that is, "Pile of ten coins" and "Pile of 20 coins"), and then the game combines them into one quantity object ("Pile of 30 coins").

 if( newitem.IsQuantity() )     DoJoinQuantities( CharacterDB.get( c.ID() ), newitemid ); 

The code simply calls the DoJoinQuantities function on an entity that holds items, to see if any existing quantity items match the type of the new quantity item.

DoJoinQuantities

The DoJoinQuantities function is a templated helper function that searches through any entity that holds items and tries to join together a given quantity item.

 template< typename entity > void DoJoinQuantities( entity& p_e, entityid p_id ) {     item keep( p_id );        // the item that is being kept     // go through the items, finding any to merge with "keep":     typename entity::itemitr itr = p_e.ItemsBegin();     while( itr != p_e.ItemsEnd() ) {         typename entity::itemitr current = itr++;         if( *current != keep.ID() ) { // make sure current item is not "keep"             item check( *current );             if( check.TemplateID() == keep.TemplateID() ) { // matching types                 keep.SetQuantity( keep.GetQuantity() + check.GetQuantity() );                 DeleteItem( check.ID() );             }         }     } } 

You pass in two values; a reference to an entity that can hold items, and the ID of the item you want others to merge into ( keep ). The loop begins and goes through every item the entity is holding. The loop must ensure that you don't accidentally merge the keep item with itself, so that's the reason for the *current != keep.ID() check.

If the item is fair game, you need to compare template IDs. Two "Pile of X coins" objects have two different IDs, but their template IDs should be the same, since they are both the same type of object. If they are the same, you need to merge them together, by setting the quantity of the item you want to keep and deleting the previous item by calling the DeleteItem helper function.

NOTE

Entities are not told when quantity items are merged or destroyed by merging. Because of this, it's really not a good idea to perform clever or complex operations with scripts on quantity items; they exist mainly to support the need of large amounts of currency-type objects in the game. The bottom line is this: If you have a quantity item with a specific ID at one point in time, you should never assume you'll have that same item later on; the quantity item may have been merged with a different object of the same type.

Example of a Transaction Destroying Entities

Before going on to more interesting things, I want to show you the last transaction example the"destroyitem" transaction in which an item is literally deleted from the game.

In the game, the transaction to destroy an entity is a special case, and the transaction should be called only sparingly. Therefore it is assumed that whenever an item is being destroyed, it is allowable .

In other words, there is no condition-check stage in this transaction; items can't tell the game, "No, please don't delete me! I deserve to LIVE!" because there are many times when a stubborn item that refuses to be destroyed screws up the logic of other actions.

Here's the code, which is pretty simple:

 void Game::DestroyItem( entityid p_item ) {     item i( p_item );     if( i.Region() == 0 ) {         character c( i.Room() );         c.DoAction( "destroyitem", p_item );         i.DoAction( "destroyitem", p_item );     }     else {         room r( i.Room() );         region reg( i.Region() );         reg.DoAction( "destroyitem", p_item );         r.DoAction( "destroyitem", p_item );         i.DoAction( "destroyitem", p_item );     }     DeleteItem( p_item ); } 

Remember, whenever an item's region is 0, it is assumed to be carried by a character.

So if the item is being carried by a character or in a room, the function must tell the actors. If it's being carried, the character and the item are both told that the entity has been destroyed; otherwise it is lying on the floor in a room somewhere, and the region, room, and item are told it's been destroyed.

Finally, the DeleteItem helper is called.

DeleteItem

The DeleteItem function is just a simple helper that removes an item from its room and region and notifies the item database to delete the item:

 void Game::DeleteItem( entityid p_item ) {     item i( p_item );     if( i.Region() ) {         region reg( i.Region() );         reg.DelItem( p_item );         room r( i.Room() );         r.DelItem( p_item );     }     else {         character c( i.Room() );         c.DelItem( p_item );     }     i.SetRoom( 0 );     i.SetRegion( 0 );     i.ClearHooks();     ItemDB.erase( p_item ); } 

Again, the function must figure out if an item is being carried by a character or lying on the ground in a room, and depending on that information, the function either deletes itself from its character, or its room and region.

The last part of code clears room and region to 0, clears the item's hooks, and finally tells the item database to erase the item (you saw how this worked in Chapter 13).

Deleting Characters

Deleting characters resembles the process of deleting items, with one significant difference: Characters force all their items into the room when they are destroyed. The room won't be consulted on whether it wants the items or not, because that kind of a thing might end up giving you zombie items . Zombies are items that exist within the game, aren't part of any room or character, but haven't been deleted. Here's the snippet of code that drops all of the items on a character (within the DeleteCharacter helper function):

 c.BeginItem(); while( c.IsValidItem() ) {     item i( c.CurrentItem() );     r.AddItem( i.ID() );     reg.AddItem( i.ID() );     i.SetRoom( r.ID() );     i.SetRegion( reg.ID() );     r.DoAction( "dropitem", p_character, i.ID(), i.GetQuantity() );     reg.DoAction( "dropitem", p_character, i.ID(), i.GetQuantity() );     c.NextItem(); } 

The item is added to the room, its positional ID is rearranged, and then the room and the region are told that the item was dropped. This ensures that any items carried by a character when it dies become part of the game, and are not lost.

Other Transactions

There are numerous other transactions included in the game module, most of which are very similar to the code I've already shown you. Here's a listing of the helper functions that execute the transactions:

 void DoCommand(         entityid p_player,              // player doing command         const std::string& p_command ); // command being executed     void Say(         entityid p_player,              // character saying something         const std::string& p_text );    // text being said     void Login(         entityid p_id );                // the ID of the character     void Logout(         entityid p_id );                        // the ID of the character     void EnterPortal(         entityid p_character,                   // character who entered         entityid p_portal );                    // portal entered from     void Transport(         entityid p_character,                   // character who left         entityid p_room );                      // room id     void ForceTransport(         entityid p_character,                   // character who left         entityid p_room );                      // room id     void GetItem(         entityid p_character,                   // character who wants item         entityid p_item,                        // item         entityid p_quantity );                  // optional quantity     void DropItem(         entityid p_character,                   // character who drops item         entityid p_item,                        // item         entityid p_quantity );                  // optional quantity     void GiveItem(         entityid p_giver,                       // character who is giving         entityid p_receiver,                    // character who is getting         entityid p_item,                        // item         entityid p_quantity );                  // optional quantity     void SpawnItem(         entityid p_itemtemplate,                // template of item         entityid p_location,                    // location to put it         entityid p_player,                      // player or room?         entityid p_quantity );                  // optional quantity     void DestroyItem( entityid p_item );        // item to destroy     void DestroyCharacter( entityid p_item );   // character to destroy     void SpawnCharacter(         entityid p_chartemplate,                // template of character         entityid p_location );                  // location to put it     void LogicAction( const Action& p_act );     void AddLogic( const Action& p_act );     void DelLogic( const Action& p_act ); 

You can see that every action maintains the same parameters as those defined by the actions in the previous chapter. For example, the "attemptgetitem" action defines the data1 , data2 , and data3 values of the Action object as the character, the item, and the quantity desired. The GetItem function takes those same parameters.

Calling the Actions

The last part of this topic is the DoAction function, which accepts an Action object and figures out which helper function to call. Here's a sample. (I've snipped most of it out for brevity.)

 void Game::DoAction( const Action& p_action ) {     if( p_action.actiontype == "chat"          p_action.actiontype == "announce" )         ActionRealmPlayers( p_action );     else if( p_action.actiontype == "vision"  )         ActionRoomCharacters( p_action, p_action.data1 );     else if( p_action.actiontype == "enterrealm" )         Login( p_action.data1 );     else if( p_action.actiontype == "leaverealm" )         Logout( p_action.data1 );     else if( p_action.actiontype == "attemptsay" )         Say( p_action.data1, p_action.stringdata );     else if( p_action.actiontype == "command" )         DoCommand( p_action.data1, p_action.stringdata );     else if( p_action.actiontype == "attemptenterportal" )         EnterPortal( p_action.data1, p_action.data2 ); ... <SNIP> ... } 

For each action type, a matching helper function is called to handle the event. Some actions are so simple that they don't even need helpers, such as the "chat" and "announce" actions; they're simply passed on to every player in the realm.

Commands

The final topic I want to brush on in this chapter is the execution of command objects, which is a special kind of action. I briefly introduced you to commands in the previous chapter, and now I'm going to show you the code that executes commands.

The first thing the command handler needs to do is grab the character that executed the command, and parse the command around a little bit:

 void Game::DoCommand(     entityid p_player,              // player doing command     const std::string& p_command )  // command being executed {     Character& c = CharacterDB.get( p_player );     std::string full = p_command;     if( full == "/" )         full = c.LastCommand();         // repeat last command     else         c.SetLastCommand( full );       // set last command     std::string command = BasicLib::ParseWord( full, 0 );     std::string args = BasicLib::RemoveWord( full, 0 ); 

Just as with the SimpleMUD, you can repeat your last command by using the / key; so your last command is loaded if you type / ; if you don't type / , your last command is reset to whatever you typed.

After the if/else clause is executed, full contains the string you want to execute. The strings command and args are filled in with the first word of the string you want to execute, and the rest of the string. So if you typed "/go north", command would hold "/go", and args would hold "north".

The next part of the code determines if a player is actually performing a command, or just chatting:

 if( !c.Quiet() && command[0] != '/' ) {         DoAction( "attemptsay", p_player, 0, 0, 0, full );         return;     }     if( command[0] == '/' )         command.erase( 0, 1 ); 

You saw from Chapter 13 that characters have a quiet mode. With quiet mode, everything a character types is assumed to be a command. If a character is not in quiet mode, anything he types is assumed to be talking, unless it starts with a / .

Table 15.1 lists the behaviors demonstrated on various inputs.

Table 15.1. Quiet Mode Behavior

Input

Quiet Mode

Loud Mode

go north

execute command go

say go north

/go north

execute command go

execute command go


After the command handler determines if a character said anything, it removes the leading / if it exists, so the command string is left with merely the command name, such as go or look , rather than /go or /look .

Here's the next part of the code that searches to see if the character has the requested command:

 try {         Character::commands::iterator itr = c.FindCommand( command );         if( itr == c.CommandsEnd() ) {             c.DoAction( "error", 0, 0, 0, 0,                         "Unrecognized Command: " + p_command );             return;         }         (*itr)->Execute( args );      // execute command     }     catch( ... ) {         c.DoAction( "error", 0, 0, 0, 0,                     "SERIOUS ERROR: Cannot execute " + command +                     ", please tell your administrator" );     } } 

The character finds the command by using its FindCommand function, which you saw in Chapter 13. If the command isn't found, the character is informed, and the function merely returns.

If the command is found, however, the function tries to execute it by passing in the arguments. Executing a script that may throw an exception could cause the whole thing to crash, which is a bad thing; eventually it will reach the network system, which catches everything and disconnects the connection if it isn't handled by then. So, I catch all errors, and whenever they occur, and I notify the character by telling him about the error.

NOTE

Ironically, just calling the DoAction function on an error can also cause an exception to be thrown, but I haven't found this to be a problem at all. If this is the case, something is probably really messed up with the character, and you're safer letting the exception travel all the way up to the network level, which discon nects the offending player. This approach minimizes the amount of corruption that a crashed script can cause.

And that's it for commands!

[ 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