Populating Your Realm with Players

[ LiB ]

Now that you've got all the classes dealing with items in the game, it's time to move on to a more complicated topic: the classes dealing with players. Players represent any person in the game who actually connects to it, as opposed to computer-controlled entities, which are a separate concept.

Player Class

Before I figure out how I'm going to store the player data to disk, I first need to know what data I need to store to disk. All this data is, of course, going to be stored within the Player class, which is found within Player.h and .cpp.

Player Variables

The class contains all the attributes I discussed in Chapter 7 and will be a child of the Entity class. In addition to having the nine standard attributes, players have variables representing non-savable session info , as well as other information. Here's a listing of the data:

 const int PLAYERITEMS = 16; class Player : public Entity {     //  Player information     string m_pass;     PlayerRank m_rank;     //  Player attributes     int m_statpoints;     int m_experience;     int m_level;     room m_room;     money m_money;     int m_hitpoints;     AttributeSet m_baseattributes;     AttributeSet m_attributes;     BasicLib::sint64 m_nextattacktime;     //  Player inventory     item m_inventory[PLAYERITEMS];     int m_items;     int m_weapon;     int m_armor;     //  Non-savable info     Connection<Telnet>* m_connection;     bool m_loggedin;     bool m_active;     bool m_newbie; }; 

In addition to a name and ID inherited from the Entity class, players have a password and a rank. The ranks correspond to the same ranks defined in Chapter 7, and the next section shows you the enumeration used to define the ranks.

There are seven extra player attributes in addition to the nine defined within the AttributeSet class. Those seven attributes represent a player's statpoints, level, experience, room number, money, current hitpoints, and the next time he may attack. (This last one won't be used until Chapter 10, "Enemies, Combat, and the Game Loop.") You may note that these attributes are not included within the AttributeSet class. There's a reason for this: Items have attribute sets, and whenever you use an item, all the attributes in the item class are added to the player's attributes. However, there are certain stats that should never be modified by an item, and those attributes are kept outside of the attribute set.

The class has two player sets: m_baseattributes and m_attributes . The first set contains all the "base" attributes, or permanent values. Whenever you use an item, these are the stats that are modified and saved to disk. The other set is the "dynamic" valuesvalues that are calculated by the game, based on your level. A player's real values are calculated by adding the values within the two sets. Figure 8.7 shows the representation of the two attribute sets. To get a player's actual attributes, the values within the base and dynamic sets are added to form the final result.

Figure 8.7. The relationship between the two sets of attribute data, and the third "virtual" attribute set.

graphic/08fig07.gif


There is one datatype in there that you may not be familiar with room . A room is basically a database pointer pointing to a Room class. Because I haven't covered the map system for the MUD yet, you have no idea what a Room or a room is yet, so this is going to cause a minor problem. However, I'm going to stipulate right here that all rooms within the game also use entityid s to be uniquely identified, and that the room class acts like a databasepointer . So for now, I temporarily inserted this line near the top of the Player.h file:

 typedef entityid room; // REMOVE THIS LATER 

I'll remove this in the next chapter, when I cover the map system.

The player's inventory is managed by four variables: an array of item database pointers, the number of items within that array, and the indexes of the player's current weapon and armor. Whenever an index in the array contains a zero, that means there's no item in that slot; any other value is the ID of the item the player is carrying in that slot. Weapon and armor index values of -1 mean that you don't have a weapon or armor armed.

Each player has four variables that are temporary and are only valid per "session"; therefore, those variables won't be saved to disk. They are Connection<Telnet>* , which represents the player's connection, and three bool s that indicate if the player is logged in, active, or a newbie.

"Activity" isn't something that I've discussed before. Throughout the game, players may temporarily "leave the realm" to accomplish a task such as editing their preferences or statistics, but they are still connected. When they are in these states, the players are said to be "inactive," meaning that the game won't send the players chat messages, and so on. I'll show you more about this later on in the chapter.

The "newbie" Boolean tells the game that the player is new to the game, and will be taken to the character-training screen when he logs in. You'll see how this works when I show you the login process.

Player Ranks

The PlayerRank type is defined as an enumerated type like this:

 enum PlayerRank {     Regular,     God,     Admin }; 

If you'll recall from Chapter 7, regular, god, and admin are the three player ranks. Like the ItemType and Attribute enumerations I covered before, PlayerRank also has two helper functions that allow you to convert ranks to and from strings ( PlayerRank GetRank( string ) and string( GetRankString( PlayerRank ) . Like before, their code isn't important, so I won't bother pasting it here.

Directly Modifiable Attributes

Throughout the game, you're going to need ways of modifying certain attributes. However, you don't want to just give access to all the attributes to anyone who feels free to change them; this could rapidly destabilize your game. For example, if some function were to randomly add 10 levels to a player (purely by accident , of course *wink*), you could get into some serious problems, since the player will not have all the stat points he should have earned by gaining those levels. Therefore, you need to restrict access to only those attributes that should be changed without side effects.

All directly modifiable attributes use a single function as their accessor. All these functions are inline and return a reference to the attribute, like this:

 inline money& Money() { return m_money; } 

As shown, you can either read or change variables using the function, like this:

 Player p; p.Money() = 100;            // modify int m = p.Money();          // read 

The directly modifiable attributes are as follows : m_pass , m_rank , m_connection , m_loggedin , m_active , m_newbie , m_statpoints , m_experience , m_room , m_money, and m_nextattacktime .

Level Functions

Several functions deal with players' levels. These functions are mainly used for informational purposes, but there's one function that performs the task of "training" a player to the next level if he has enough experience to merit the training.

Informational Level Functions

First and foremost is the NeedForLevel() function. This simply determines how many experience points a player needs for a specific level:

 inline int Player::NeedForLevel( int p_level ) {     return (int)(100 * ( pow( 1.4, p_level - 1 ) - 1 )); } 

This uses the formula I showed you from Chapter 7. You should note that this function is static, which means that you don't need a specific player instance to call it. You can simply use Player::NeedForLevel( 5 ) to find out how many experience points are needed for level 5.

The next function determines how many more experience points a player needs to advance to the next level:

 int Player::NeedForNextLevel() {     return NeedForLevel( m_level + 1 ) - m_experience; } 

The function simply subtracts the experience you have from the experience you need for the next level. Because of this, the result may be positive (you need more experience) or negative (you have enough experience for the next level).

The final informational function is the Level function, which returns a player's current level:

 inline int Level() { return m_level; } 

Training

The last level function is the Train function, which is executed whenever your player trains inside a training room. It returns a Boolean, which tells you if the player trained to the next level successfully. Whenever you train, you gain more stat points and your attributes are recalculated. Here's the function:

 bool Player::Train() {     if( NeedForNextLevel() <= 0 ) {         m_statpoints += 2;         m_baseattributes[MAXHITPOINTS] += m_level;         m_level++;         RecalculateStats();         return true;     }     return false; } 

If you don't need more experience to go to the next level, you're awarded two stat points, your base maximum hitpoints are increased by the value of your current level, your level is increased by one, and your stats are recalculated.

If you don't have enough experience to go to the next level, false is returned, and nothing is changed.

Attribute Functions

Attribute functions can be separated into four groups: the recalculation function, hitpoint functions, attribute set functions, and general accessors. These function groups deal with all the attributes that the modifiable-attribute accessor functions didn't take care of already.

Recalculating Stats

You've already seen the function to recalculate stats used before, inside the Train function. Essentially , the function goes through all the dynamic attributes ( m_attributes ) and recalculates them based on your level or other attributes that may have changed.

 void Player::RecalculateStats() {     m_attributes[MAXHITPOINTS] = (int)         10 + ( m_level * ( GetAttr( HEALTH ) / 1.5 ) );     m_attributes[HPREGEN] =         ( GetAttr( HEALTH ) / 5 ) + m_level;     m_attributes[ACCURACY] = GetAttr( AGILITY ) * 3;     m_attributes[DODGING] = GetAttr( AGILITY ) * 3;     m_attributes[DAMAGEABSORB] = GetAttr( STRENGTH ) / 5;     m_attributes[STRIKEDAMAGE] = GetAttr( STRENGTH ) / 5;     // make sure the hitpoints don't overflow if your max goes down:     if( m_hitpoints > GetAttr( MAXHITPOINTS ) )         m_hitpoints = GetAttr( MAXHITPOINTS );     if( Weapon() != 0 )         AddDynamicBonuses( Weapon() );     if( Armor() != 0 )         AddDynamicBonuses( Armor() ); } 

Essentially the Train function uses the formulas I showed you in Chapter 7 to calculate the values of six of your attributes. Pay attention to the last four lines of code, which call a helper called AddDynamicBonuses ; essentially this takes the bonuses of a player's current weapon and armor and adds them to his stats. I haven't gone over player item functions yet, but this is simple enough to understand.

This functionality is contained within a single function for a good reason. MUDs were designed to be tinkered with, and as such, they should be designed with the utmost flexibility. Whenever you change your strength, it's not a good practice to manually change the DAMAGEABSORB and STRIKEDAMAGE attributes within the function that changed the strength; this can lead to errors. It is always better to keep your formulas in one place throughout the entire game.

Also, you could end up later on making a certain core attribute affect another attribute, so it's good to have one place in the code that can take care of all changes.

It is important to note that the three core attributes (strength, health, and agility) aren't affected by any of the other six attributes, and don't have dynamic values. It is also important to note that six non-core attributes do not affect other attributes either. If you do end up making the attributes affect each other, you may end up introducing bugs into your code, depending on the order that your stats are recalculated.

Hitpoint Functions

There are two functions dealing with hitpoints:

 inline int HitPoints() { return m_hitpoints; } void Player::AddHitpoints( int p_hitpoints ) {     m_hitpoints += p_hitpoints;     if( m_hitpoints < 0 )         m_hitpoints = 0;     if( m_hitpoints > GetAttr( MAXHITPOINTS ) )         m_hitpoints = GetAttr( MAXHITPOINTS ); } 

The HitPoints function is a simple nonmodifiable accessor, which simply returns the player's current hitpoints.

The AddHitpoints function is an "adding" function. I decided that instead of a direct "Set value" function, it would be easier to "add" to the value. For example, throughout the game, you're much more likely to be adding deltas to a player's hitpoints. The following code depicts the "set" versus "add" methods when adding 10 hitpoints to a player:

 player.SetHitpoints( player.Hitpoints() + 10 ); // function doesn't exist player.AddHitpoints( 10 );  // much easier to use 

Obviously, the "add" method is far more usable. By the way, if you want to subtract, you can easily use a negative number in the parameters.

The modification function handles some extra work in addition to modifying the hitpoints. To keep the game consistent, the modification function makes sure that your hitpoints never go below 0 or above your maximum amount.

Attribute Functions

Four attribute functions operate on the nine attributes, which exist within AttributeSet s. You've already seen one attribute used in a few functions: GetAttr .

 inline int Player::GetAttr( int p_attr ) {     int val = m_attributes[p_attr] + m_baseattributes[p_attr];     if( p_attr == STRENGTH  p_attr == AGILITY  p_attr == HEALTH ) {         if( val < 1 )    return 1;     }     return val; } 

As you saw earlier in Figure 8.7, this function generally adds the dynamic and the base values of an attribute together to obtain the final result. There is one special case that the function needs to look out for, however. Officially, a character's core attributes should never fall below 1; the game has undefined behavior if that happens. The problem is that some people may accidentally use a bunch of "cursed" items that lower their base attributes below 1. So, to fix that, the " reported " value of any negative core attribute will be 1, no matter what its "real" value is.

You can also obtain the value of just a base attribute:

 inline int Player::GetBaseAttr( int p_attr ) {     return m_baseattributes[p_attr]; } 

I haven't found a pressing need to have a function that retrieves the dynamic attribute values alone, but if a need ever arises, you can simply subtract the base value from the total value.

The final two functions are called SetBaseAttr and AddToBaseAttr , which, as you can imagine, set and add to any of your base attributes:

 void Player::SetBaseAttr( int p_attr, int p_val ) {     m_baseattributes[p_attr] = p_val;     RecalculateStats(); } void Player::AddToBaseAttr( int p_attr, int p_val ) {     m_baseattributes[p_attr] += p_val;     RecalculateStats(); } 

Whenever one of your attributes is changed, these functions also automatically call RecalculateStats , to update the dynamically calculated stats of your player.

Other Attribute Functions

The remaining four attribute functions are the StatPoints , Experience , CurrentRoom , and Money functions. They return references so you can modify their values however you please .

Item Functions

There are several player functions that deal with items in your inventory. They range from simple accessors to helper functions, and to functions that physically modify your inventory.

Accessors

There are five item accessor functions. The first three of those are simple:

 inline item GetItem( int p_index )      { return m_inventory[p_index]; } inline int Items()                      { return m_items; } inline int MaxItems()                   { return PLAYERITEMS; } 

These functions return an item representing an item within your inventory, the number of items in your inventory, and the maximum number of items you can have in your inventory.

The other two accessors retrieve item s representing your current weapon and armor:

 inline item Player::Weapon() {     if( m_weapon == -1 )                // if no weapon armed         return 0;                       // return 0     else         return m_inventory[m_weapon];   // return item id } inline item Player::Armor() {     if( m_armor == -1 )                 // if no armor armed         return 0;                       // return 0     else         return m_inventory[m_armor];    // return item id } 

The Weapon and Armor functions actually return an item (a database pointer object), pointing to the item that those variables represent. Obviously, if either of those variables is -1 (meaning that you don't have a weapon or an armor equipped), the item returned is equivalent to 0, which is the invalid ID for all entities.

Helpers

There are two item helper functions: one to add temporary item bonuses to a player, and one to add permanent item bonuses to your base attributes. You've already seen the first one used:

 void Player::AddDynamicBonuses( item p_item ) {     if( p_item == 0 )          // make sure item is valid         return;     Item& i = *p_item;         // get reference     for( int x = 0; x < NUMATTRIBUTES; x++ )         m_attributes[x] += i.GetAttr( x );  // add each attr } 

The first function loops through every index in your m_attributes attribute set (the temporary set, not the base set), and adds each attribute from the item. This function is only meant to be called from within RecalculateStats .

The other function adds permanent bonuses:

 void Player::AddBonuses( item p_item ) {     if( p_item == 0 )            // make sure item is valid first         return;     Item& itm = *p_item;         // get ref to actual Item object     for( int i = 0; i < NUMATTRIBUTES; i++ ) {         m_baseattributes[i] += itm.GetAttr( i ); // add each attribute     }     RecalculateStats(); } 

This function calls RecalculateStats at the end, because you want to update everything after you've modified the attributes.

Inventory Modification

Two functions deal with modifying a player's inventory by physically adding or removing items to or from it. Since it's easier to add items than remove them, I'll show you the add function first:

 bool Player::PickUpItem( item p_item ) {     if( m_items < MaxItems() ) {         item* itr = m_inventory;         while( *itr != 0 )             ++itr; 

The previous code segment finds the first place in the inventory with an open slot. Since the function checks to make sure that you are carrying less than the maximum number of items, this loop should always find an open slot. (If it doesn't, you're in deep trouble as it is, so this function wouldn't be able to fix it anyway.) Here's the second half:

 *itr = p_item;         m_items++;         return true;     }     return false; } 

The item is inserted into your inventory, your item count goes up, and the function returns true . If there is no room, then nothing is inserted, and false is returned.

The next function removes an item from your inventory; the difference is that instead of passing in an item ID as the parameter, you're now passing the index of the item within your inventory array. So if you want to remove the item at index 0 (no matter what item ID it has), you'd pass in 0.

 bool Player::DropItem( int p_index ) {     if( m_inventory[p_index] != 0 ) {         if( m_weapon == p_index )             RemoveWeapon();         if( m_armor == p_index )             RemoveArmor();         m_inventory[p_index] = 0;         m_items--;         return true;     }     return false; } 

This function first checks to see if 0 exists at the index you want to remove. If it does, you obviously cannot remove it (since it doesn't exist!), so false is returned. If a valid item exists at that index, however, the function continues.

If the item is either your current weapon or your current armor, it is removed from your person by calling the RemoveWeapon() or RemoveArmor() functions. Then a 0 is inserted into your inventory, and the number of items you are carrying is reduced.

Weapon and Armor Modification

You may recall from Chapter 7 that players can have a single weapon and a single piece of armor that is armed , which means that the player is holding a specific weapon or wearing a specific piece of armor. To deal with this, there are four functions; two of them disarm something, and two of them arm something. Since it's easier to remove stuff, I'll show you one of the removal functions first:

 void Player::RemoveWeapon() {     m_weapon = -1;     RecalculateStats(); } 

The RecalculateStats helper function is called after a weapon has been removed, so that the player's stats can be updated.

Arming an item is a little more difficult, because if an item is already armed, it must first be disarmed:

 void Player::UseWeapon( int p_index ) {     RemoveWeapon();     m_weapon = p_index;     RecalculateStats(); } 

Again, the armor function is virtually identical, so I'm not going to paste it here.

Item Searching

At times within the game, you're going to need to search for items within a player's inventory based on a string name. Instead of forcing you to manually perform this kind of lookup, I've included a function that returns the index of an item that matches a name:

 int Player::GetItemIndex( const std::string& p_name ) {     item* i = double_find_if( m_inventory,                               m_inventory + MaxItems(),                               matchentityfull( p_name ),                               matchentity( p_name ) );     if( i == m_inventory + MaxItems() )         return -1;     return i - m_inventory;  } 

You know, I really love this piece of code. It is incredibly beautiful. As you can see, m_inventory is just a regular array of item s (which are databasepointer s), but the double_find_if algorithm still works on it!

In any STL algorithm, a pointer to an array acts just like an iterator, so you can pass m_inventory as the starting iterator, and m_inventory + MaxItems() (which points to the address just past the end of the array) as the ending iterator.

The function performs a double-pass search on your inventory array, first trying to fully match the name, and then to partially match it. If there was no match, the function returns a pointer to the item inside the array that matches the name, or m_inventory + MaxItems() . If there was no match, -1 is returned, indicating that nothing was found inside the inventory.

Finally, if an item was found, a little bit of pointer math is used: i - m_inventory . Since both values are pointers, the difference between those two pointers is the number of indexes between them. So the function returns the index of an item within the inventory, matching your string.

Constructor

Like all good classes, the Player class has a constructor. This clears the variables inside of the class to values that represent a brand new player. Here's the code:

 Player::Player() {     m_pass = "UNDEFINED";     m_rank = REGULAR;     m_connection = 0;     m_loggedin = false;     m_active = false;     m_newbie = true;     m_experience = 0;     m_level = 1;     m_room = 0;     m_money = 0;     m_baseattributes[STRENGTH] = 1;     m_baseattributes[HEALTH]   = 1;     m_baseattributes[AGILITY]  = 1;     m_statpoints = 18;     m_items = 0;     m_weapon = -1;     m_armor = -1;     RecalculateStats();     m_hitpoints = GetAttr( MAXHITPOINTS ); } 

As per Chapter 7, all three of your core stats start at 1, and the number of stat points you have is set to 18. Once all the core and base stats are set, all your other stats are set using the RecalculateStats() helper function.

The final step is to set your current hitpoints equal to your maximum hitpoints.

Communication

Obviously, since every connection in the game is tied to a player, and every player has a connection pointer, the Player class is going to be in charge of how the game communicates information back to the clients . For the Player class to be in charge, every player has a function named SendString , which sends a string of text to the player:

 void Player::SendString( const std::string& p_string ) {     // make sure the player is connected:     if( Conn() == 0 ) {         ERRORLOG.Log( "Trying to send string to player " +                       Name() + " but player is not connected." );         return;     }     // send the string plus a newline:     Conn()->Protocol().SendString( *Conn(), p_string + newline );     // send the statbar if the player is active:     if( Active() ) { PrintStatbar(); } } 

This is just a simple function that helps manage what is sent to a player's connection. If a string is accidentally sent to a player who isn't connected, that event logs an error in the error log and returns without doing anything. Stuff like that shouldn't happen, but if it does, you don't want your program to unexpectedly crash and potentially lose data.

If the connection is active, the function uses its current Telnet protocol object to send the string to the user . It also tacks on a newline at the end.

Finally, if the player is active within the game, his status bar is also printed out to the connection, so that the player can see his vital stats whenever something happens in the game.

The status bar function is simple, but it is somewhat long; for that reason, I am going to refrain from showing it here. All you need to know is that the status bar function prints out the status of the player in a [current hitpoints/max hitpoints] format.

Functors

There are three player functors. Two of these are predicates , just like the matchentity and matchentityfull functors you saw earlier, and one is a unary function .

The two predicate player functors determine if a player is active or logged in. Here's the active functor:

 struct playeractive {     inline bool operator() ( Player& p_player ) {         return p_player.Active();     }     inline bool operator() ( Player* p_player ) {         return p_player != 0 && p_player->Active();     } }; 

As you can see, the active functor is structurally similar to the previous two, so I'm not going to spend any more time explaining it. The other predicate functor is playerloggedin , which simply checks if a player is logged in.

Designed to be used in STL algorithms such as std::for_each , the third functor performs an operation on every object in a collection:

 struct playersend {     const string& m_msg;     playersend( const string& p_msg )         : m_msg( p_msg ) { /* do nothing */ }     void operator() ( Player& p ) {         p.SendString( m_msg );     }     void operator() ( Player* p ) {         if( p != 0 )  { p->SendString( m_msg ); }     } }; 

This functor simply sends a string to a player. Assuming you had an array of 16 players named parray , you could use it like this:

 std::for_each( parray, parray + 16, playersend( "Hello!" ) ); 

File Functions

The final two functions load and save players to a specific file. Like the Item class, the Player class knows how to read and write to streams.

 friend ostream& operator<<( ostream& p_stream, const Player& p ); friend istream& operator>>( istream& p_stream, Player& p ); 

The functions are structurally similar to the Item functions performing the same task: The stream insertion routine ( operator<< ) goes through every attribute, writes out its name in square brackets, and then writes out its value. The stream extraction routine assumes that all the variables are in a specific order, and ignores the attribute labels on each line.

Here's a sample of the insertion function:

 inline ostream& operator<<( ostream& p_stream, const Player& p ) {     p_stream << "[NAME] " << p.m_name << "\n";     p_stream << "[PASS] " << p.m_pass << "\n"; 

The function continues in the same manner for these variables, in this order: m_name , m_pass , m_rank , m_statpoints , m_experience , m_level , m_room , m_money , m_hitpoints , m_nextattacktime , and m_baseattributes . The attack time variable deserves special mention, because VC6 doesn't support streaming 64-bit integers, so I need to use the BasicLib::insert function to insert the value:

 p_stream << "[NEXTATTACKTIME] "; insert( p_stream, p.m_nextattacktime ); 

After that, the inventory is written out, and this is a special exception:

 p_stream << "[INVENTORY] ";     for( int i = 0; i < p.MaxItems(); i++ ) {         p_stream << p.m_inventory[i].m_id << " ";     }     p_stream << "\n"; 

For the inventory, all 16 items are written on the same line, with a single space separating each one. Remember: item database pointers know how to write themselves to streams. They write their ID number. Once those are written, the final two attributes are written: m_weapon , and m_armor .

The stream extraction routine is similar. Here are a few lines to give you a taste:

 inline istream& operator>>( istream& p_stream, Player& p ) {     std::string temp;     p_stream >> temp >> std::ws;    std::getline( p_stream, p.m_name );     p_stream >> temp >> p.m_pass;     p_stream >> temp >> temp;       p.m_rank = GetRank( temp ); <SNIP>     p_stream >> temp; extract( p_stream, p.m_nextattacktime );     p_stream >> p.m_baseattributes;     p_stream >> temp;     p.m_items = 0;     for( int i = 0; i < p.MaxItems(); i++ ) {         p_stream >> p.m_inventory[i].m_id;         if( p.m_inventory[i] != 0 )  { p.m_items++; }     } <SNIP>     return p_stream; } 

I snipped out most of the uninteresting code and left the important parts . This code should remind you of the Item class extraction code, but there's one important addition. When a player's items are loaded, the function automatically counts how many items are in his inventory and updates its m_items variable. This saves you the trouble of writing the value out to disk and possibly introducing bugs into your program.

Player Function Listing

Table 8.6 shows a listing of all the Player class functions for easy reference.

Table 8.6. Player Functions

Function

Purpose

Player()

Constructs a new player.

int NeedForLevel( int level )

Calculates the experience total needed for 'level'.

int NeedForNextLevel()

Calculates how much more experience a player needs for the next level.

bool Train()

If a player has enough experience, this takes him to the next level and recalculates his stats. If not, false is returned.

int Level()

Returns a player's level.

void RecalculateStats()

Recalculates a player's stats.

void AddHitpoints( int h )

Adds 'h' to a player's hitpoints, but doesn't exceed the player's max hitpoints, or below 0.

int HitPoints()

Returns a player's current hitpoints.

int GetAttr( int attr )

Returns the full value of a player's attribute 'attr'.

int GetBaseAttr( int attr )

Returns the base value of a player's attribute 'attr'.

void SetBaseAttr( int attr, int val )

Sets the base value of 'attr' and recalculates stats.

void AddToBaseAttr( int attr, int val )

Adds 'val' to base value of 'attr' and recalculates stats.

int& StatPoints()

Returns the number of statpoints a player has.

int& Experience()

Returns the experience of a player.

room& CurrentRoom()

Returns a player's room number.

money& Money()

Returns a player's money.

sint64& NextAttackTime()

Returns the game time at which a player can next attack.

item GetItem( int index )

Returns the ID of the item at 'index' in inventory.

int Items()

Returns the number of items in inventory.

int MaxItems()

Returns the number of items a player can hold at max.

item Weapon()

Returns the ID of current weapon.

item Armor()

Returns the ID of current armor.

void AddBonuses( item i )

Adds item i's bonuses to a player's base attributes.

void RemoveBonuses( item i )

Removes item i's bonuses from a player's base attributes.

bool PickUpItem( item i )

Makes the player attempt to add an item to his inventory. Returns false on failure (not enough room).

bool DropItem( int index )

Makes the player attempt to remove an item from his inventory. Returns false on failure (item doesn't exist).

void RemoveWeapon()

Removes the player's current weapon. (The weapon stays in inventory.)

void RemoveArmor()

Removes the player's current armor. (The armor stays in inventory.)

void UseWeapon( int index )

Attempts to use a weapon in a player's inventory.

void UseArmor( int index )

Attempts to use armor in a player's inventory.

int GetItemIndex( string name )

Attempts to find the index of the item with 'name'; returns -1 if not found.

string& Password()

Returns a player's password.

playerRank& Rank()

Returns a player's rank.

connection<Telnet>*& Conn()

Returns a player's connection pointer.

bool& LoggedIn()

Returns whether a player is logged in.

bool& Active()

Returns whether a player is active.

bool& Newbie()

Returns whether a player is a newbie.

void SendString( string str )

Sends 'str' to a player's connection.

void PrintStatBar()

Prints a player's statbar to a player's connection.


Disk Storage

Using text files to store data is a tricky task. The problem arises when you come across the fact that text data is constantly changing in size. For example, let's assume that you have 500 players in your game, all stored within one file, just as items are stored. One of the players who started at the beginning of the game, number 10 or so, had a health of 8 when the file was saved. Then he went into the game, played around a while, and gained three more health points, giving him 11. At that point, the player wanted to quit, and his character was saved back to disk.

But you have a problem. The player's health is now 2 characters long instead of just 1, so you need more room for that data. You're going to have to move everything after the player's position down one byte, and doing that within a file is a ridiculously slow and awkward operation.

With binary files, you usually don't have that problem; you know exactly how much space each number takes up, and you usually limit your strings to a certain length.

The most common solution for this problem is to use separate files for each player. That way, whenever a player is saved to disk, the program can safely overwrite the entire file, without worrying about affecting other files. This is the approach I'm taking. Players are stored within a subdirectory named /players/, and every player file is named "name.plr". So, JohnDoe's file would be stored in "/players/JohnDoe.plr".

Of course, it wouldn't be life without a little snag thrown in for good measure. The C++ standard doesn't define a method to retrieve file names from a specific directory; it actually just assumes you know what files you'll need. So you can't sort through the /players/ directory and pick out all .plr files and load them. Instead, you need another file, which contains the names of the player files within the directory. This file is also within the / players/ directory, and is called "players.txt". This file simply contains a list of all player files to be loaded when the game starts up, like so:

 JohnDoe.plr RonPenton.plr 

And so on. Figure 8.8 shows this in action, as well as the organization of the files within the /player/ directory of the SimpleMUD. Players' names are stored within a file named "players.txt", and the files that store the players are named "player.plr", where "player" is the player's actual name.

Figure 8.8. The organization of the files within the /player/ directory of the SimpleMUD.

graphic/08fig08.gif


Whenever a new player is added within the game, the software automatically appends the filename to the end of the players.txt file. The only downside is that if you create your own players, you must remember to add their file names to the text file, otherwise , the MUD has no idea that the players exist. Creating your own players isn't recommended, but as the runner of the MUD, you're certainly entitled to do so.

PlayerDatabase

The database that stores players is just like the database that stores items. The PlayerDatabase is a child class of the EntityDatabase class and is stored in the PlayerDatabase.h and PlayerDatabase.cpp files.

In addition to loading players, the PlayerDatabase must know how to save the players back to disk.

Class Declaration

Here's the class declaration:

 class PlayerDatabase : public EntityDatabase<Player> { public:     static bool Load();     static bool Save();     static bool AddPlayer( Player& p_player );     static inline string PlayerFileName( const string& p_name );     static void LoadPlayer( string p_name );     static void SavePlayer( entityid p_player );     static entityid LastID();     static iterator findactive( const std::string& p_name );     static iterator findloggedin( const std::string& p_name );     static void Logout( entityid p_player ); }; 

As you can see, the class adds no new data and has functions to load/save the entire database, and to load/save individual players. Additionally, there's a function to add new players to the database, and a whole bunch of helper functions as well.

Loading the Database

The LoadDatabase function basically loads the player's players.txt file and attempts to load every player listed in the file. If the player already exists within the database, he is essentially overwritten. Here's the code:

 bool PlayerDatabase::Load() {     ifstream file( "players/players.txt" );     string name;     while( file.good() ) {         // while there are players         file >> name >> std::ws;   // load in the player name         LoadPlayer( name );        // call the LoadPlayer helper function     }     return true; } 

The players.txt file is read in. Then the player file names are read into name one by one (with whitespace "eaten" using std::ws ), and loaded from disk into the database. This function uses the LoadPlayer helper function, which takes a player's name and loads that player from disk.

It may seem awkward to need a helper function for that, but it has to do with the way I've set up player file names. Examine the source code for the helper:

 void PlayerDatabase::LoadPlayer( string p_name ) {     entityid id;     string temp;     p_name = PlayerFileName( p_name );      // create the proper file name     ifstream file( p_name.c_str() );        // open the file     file >> temp >> id;                     // load the ID     m_map[id].ID() = id;     file >> m_map[id] >> std::ws;           // load the player from the file     USERLOG.Log( "Loaded Player: " + m_map[id].Name() ); } 

The C++ file stream classes cannot be constructed with an std::string as the file name. This is probably because file streams were created before the string class was standardized, but that doesn't matter: It creates a problem. File streams expect char* s for the file names, and you can't pass strings into the constructors. In addition, strings cannot be automatically converted into char* s using conversion operators. (Believe methat's a whole other can of worms.) You have two options: You can use the antiquated C-style methods to create the proper file name, or you can accept this limitation and create a helper function to handle the loading for you. I'll get to that in a bit.

Like the ItemDatabase class, the database is responsible for loading the IDs and then loading in the actual player.

I'm a huge fan of code flexibility, and the player file name helper-function helps quite a bit. Some people may complain, "Ah, but there are 100 functions all over the place," but they've probably never worked on projects requiring constant change. I'll show you exactly what I mean in a bit. This loader function takes a player's name and loads that player in from disk into a Player object. You can spot the flexibility in the code by looking at the PlayerFileName function, which is another helper:

 inline string PlayerFileName( string& p_name ) {     return string( "players/" + p_name + ".plr" ); } 

It's just a simple one-line function. "What the heck does that need a whole function for?" you may ask. Consider this: Throughout the database class, you may need to construct the file name of a player from his name. You could take the easy way out and put "players/" + name + ".plr" in about 20 places in your code, or you could leave all the code that creates a file name in one central location. Imagine your embarrassment if you accidentally spell the directory name "player/", leaving the 's' out. How long would it take you to track that down or even to notice it in the first place? And what if, later on, you want to change the directory name to something else? You'd need to search for every instance in which you create a file name and change it. Blah! Too many bugs are created that way. Programming is the art of trying to avoid doing work. Trust me.

At this point, you might be saying, "But too many function calls make the game slow!" That's possible but unlikely . The function is inlined , which means that you're telling the compiler to optimize it sort of like a macro. If you're not familiar with this stuff, I explain it in Appendix C, which is on the CD. The bottom line is this: Your compiler is really smart, and will probably optimize the function better than you could manually. Trust in your compiler!

Saving the Database

Saving the database is simple as well. The process opens up and destroys players.txt and then rewrites all the names of the players into the database (just for safety's sake; you can never tell when your files might get corrupted by accident). The process also saves every player to his own file.

 bool PlayerDatabase::Save() {     ofstream file( "players/players.txt" );     iterator itr = begin();     while( itr != end() ) {           // loop through every player         file << itr->Name() << "\n";  // write the player's name         SavePlayer( itr->ID() );      // save the player         ++itr;                        // go to the next player     }     return true; } 

NOTE

The function that saves the data base always returns true , because I didn't take the time to do proper error checking here, in case you run out of disk space, or if any other unexpected error occurs. In a more robust system, it's advisable to check if there were any errors, and take appropriate action based on

There's nothing too special about the function; it simply uses the internal iterator class to loop through every player in the database.

Saving a Single Player

Saving a single player to disk requires only an ID:

 void PlayerDatabase::SavePlayer( entityid p_player ) {     std::map<entityid, Player>::iterator itr = m_map.find( p_player );     if( itr == m_map.end() )   return;     std::string name = PlayerFileName( itr->second.Name() );     ofstream file( name.c_str() );     file << "[ID]             " << p_player << "\n";     file << itr->second; } 

If you're trying to save a player who doesn't exist to disk, the function can fail. Other than that, you're pretty much homefree. The function makes sure to write out the ID of a player before writing out the actual player.

Adding New Players

Whenever a new player logs into the MUD, the game needs to be able to add new players to the database. Since the player database is very closely linked with the representation of players on your hard drive, the function to add new players should take steps to ensure that all relevant information about the player is written to disk immediately.

So whenever a new player is added to the database, his file name is added to players.txt, and the initial state of the player is written out to disk. The function also has a few precautionary measures to make sure players aren't duplicated . Here's the code:

 bool PlayerDatabase::AddPlayer( Player& p_player ) {     if( Has( p_player.ID() ) )                // make sure ID doesn't exist         return false;     if( HasFullName( p_player.Name() ) )      // make sure name doesn't exist         return false;     m_map[p_player.ID()] = p_player;          // insert player into database     std::ofstream file( "players/players.txt", std::ios::app );     file << p_player.Name() << "\n";          // add player's name to file     SavePlayer( p_player.Name(), p_player );  // write player to disk     return true; } 

As you can see from the code, the game checks to see if a player's ID or name already exists within the database. If either of them already exists, the function fails, and the player isn't added.

If both of the tests succeed, the player is added into the database, and players.txt is opened in "append" mode, which preserves the contents and allows you to write to the end of the file. The name of the player's datafile is written to the player's .txt file, and finally the player is saved to his own .plr file.

Searching the Database

Two functions search the database for a playerthe findactive and findloggedin functions which search the database for players who are active or logged in. These functions are pretty simple, actually, because of the functors I defined earlier in conjunction with the double_find_if algorithm I defined in the BasicLib . Here's the code:

 static iterator findactive( const std::string& p_name ) {     return BasicLib::double_find_if(         begin(), end(), matchentityfull( p_name ),         matchentity( p_name ), playeractive() ); } static iterator findloggedin( const std::string& p_name ) {     return BasicLib::double_find_if(         begin(), end(), matchentityfull( p_name ),         matchentity( p_name ), playerloggedin() ); } 

These two functions utilize the 5-parameter version of double_find_if , which takes 2 iterators and 3 predicate functors. In the 4-parameter version, only 2 functors are used one for the first pass and one for the second pass. The 5-parameter version uses the first functor ( matchentityfull ) on the first pass in conjunction with the third functor ( playeractive or playerloggedin ), and then uses the second functor for the second pass, again in conjunction with the third functor.

When the findactive function performs the first pass, it checks to see if the name matches fully and if the player is active. If neither of those conditions is true, the algorithm keeps searching. The end result is that the findactive function finds a player who matches the given name and is also active. The findloggedin function does the same for logged in players.

Logging Out

The player database class also has a function that performs all the operations that are needed to successfully log a player out of the game:

 void PlayerDatabase::Logout( entityid p_player ) {     Player& p = get( p_player );     USERLOG.Log(         SocketLib::GetIPString( p.Conn()->GetRemoteAddress() ) +           " - User " + p.Name() + " logged off." );     p.Conn() = 0;     p.LoggedIn() = false;     p.Active() = false;     SavePlayer( p_player ); } 

The user log is told that the player is logging out, the connection is cleared, and both the logged in and active Booleans are cleared. The final act is to save the player to disk. It's always a good idea to frequently save players to disk in case of disaster.

Database Pointers

There is one final aspect of the player database that I have not yet covered: the database pointers. Since I covered the concepts of such structures earlier, there is no need to go into detail about them again. Here are the player macro definitions:

 // in DatabasePointer.h: DATABASEPOINTER( player, Player ) // in DatabasePointer.cpp: DATABASEPOINTERIMPL( player, Player, PlayerDatabase ) 

This code simply allows you to use the player datatype like a pointer into the player database. As with the item and Item classes, pay attention to the capitalization.

[ LiB ]


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

Similar book on Amazon

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