Databases

[ LiB ]

In the last chapter, I went over the various database base classes: Database , VectorDatabase , MapDatabase , and TemplateInstanceDatabase . None of these databases actually stored any entities, however; they just provided the framework for the actual entity databases within the BetterMUD: CharacterDatabase , ItemDatabase , RoomDatabase , PortalDatabase , RegionDatabase , and AccountDatabase .

The beautiful part about all these classes is that they are simple. Heck, the RoomDatabase is so insanely simple that it doesn't add a single member variable or function to the VectorDatabase that it inherits from!

The two databases that hold volatile entities ( characters and items) inherit from the TemplateInstanceDatabase class, which means that they have a vector of templates and a map of instances.

Accounts are stored in a map database; portals, rooms, and regions are all stored in vector databases.

This means that looking up item and character templates, portals, rooms, and regions is fast. It's a little bit slower to look up item and character instances and accounts, but it's not that slow. If you think about it, even with a completely full map database of 4-billion entities, it takes around 32 comparisons to find any item in the database.

I don't want to spend much more time going over the databases, simply because the database system for the BetterMUD is quick and dirty.

In reality, you can swap out all these classes with more professional versions, if the need ever arises. The problem with these "databases" is that they keep everything in memory, which means that your game is ultimately limited by how much memory you have on the system at any given time. While that's not such a big deal when you start out, multiple hundreds of megabytes eventually seems limiting, if your MUD gets large enough. That's when you should start considering adding in a real database.

Account Database

The database that stores accounts is mildly complex. The main purpose of this database is to manage accounts, and that's precisely what it does. Here's the class skeleton:

 class AccountDatabase : public MapDatabase<Account> { public:     entityid Create( const string& p_name, const string& p_pass );     bool AcceptibleName( const string& p_name );     void Load();     void Save(); }; extern AccountDatabase AccountDB; 

You should also note that I've created a global instance of the database named AccountDB . Remember that the databases from the SimpleMUD were all static classes, but they're globals in the BetterMUD. It's really just semantics; after working with the SimpleMUD for a while, I found that I was constantly annoyed by typing MonsterDatabase::blah() . In the BetterMUD, you access the databases by calling the global instance instead, such as AccountDB.Save() .

The account database can do three things above and beyond what a MapDatabase provides: create new accounts, test if various names are acceptable names for an account, and provide operations for loading and saving all accounts from and to disk.

Creating New Accounts

The account creation function is simple:

 entityid AccountDatabase::Create( const string& p_name,     const string& p_pass ) {     entityid id = FindOpenID();     Account& a = m_container[id];     a.SetID( id );     a.SetName( p_name );     a.SetPass( p_pass );     a.SetLoginTime( g_game.GetTime() );     return id; } 

This code gets an open ID from the map, creates the new account by retrieving it using the [] operator, and then sets all the information for it. Finally, the ID is returned.

Checking Name Validity

The account database's AcceptibleName function is the same as those of the SimpleChat and SimpleMUD, so I'm not going to bother showing the function here. The gist is that this function can't accept names with the following characteristics: weird characters such as " \"'~!@#$%^&*+/\\[]{}<>()=.,?;:"; fewer than 3 characters; longer than 16 characters, names that don't start with an alphabetic character; and names that are not equal to "new" (which screws up the login system).

Loading and Saving

Loading and saving the account database is a simple affair, especially loading:

 void AccountDatabase::Load() {     LoadDirectory( "data/accounts/" ); } 

This code simply wraps around the Database::LoadDirectory function, which automatically loads up a manifest file in the directory, and proceeds to load as many entities as it can, automatically inserting them into the database as it goes.

Saving is a bit more complex, since there's no solid definition in the Database class that shows how datafiles are spread out:

 void AccountDatabase::Save() {     // load up the manifest file:     static std::string dir = "data/accounts/";     static std::string manifestname = dir + "manifest";     std::ofstream manifest( manifestname.c_str(), std::ios::binary );     container::iterator itr = m_container.begin();     // loop while there are accounts, saving each one to its own file:     while( itr != m_container.end() ) {         std::string accountfilename = dir + itr->second.Name() + ".data";         std::ofstream accountfile( accountfilename.c_str(),                                    std::ios::binary );         SaveEntity( accountfile, itr->second );         // add an entry to the manifest:         manifest << itr->second.Name() << "\n";         ++itr;     } } 

The function loads up the manifest (which clears its contents since you're overwriting the existing contents of the file). Then the function loops through all accounts and saves a file for each, and also adds the name of the account to the manifest file.

Character Database

The character database is a template/instance database that stores characters and character templates. Seems obvious in retrospect, doesn't it?

This database can be queried to find players (a special subset of characters, representing anyone who can log in and play the game), save players to disk, load individual players, load all templates, and load individual templates:

 class CharacterDatabase :     public TemplateInstanceDatabase<Character, CharacterTemplate> { public:     bool HasName();     entityid FindPlayerFull( const std::string& p_name );     entityid FindPlayerPart( const std::string& p_name );     void SavePlayers();     void LoadPlayers();     void LoadTemplates();     void LoadTemplates( const std::string& p_file );     void LoadPlayer( const std::string& p_name ); }; extern CharacterDatabase CharacterDB; 

Characters are a special type of entity in the BetterMUD; they can exist as computer-controlled objects, or they can exist as players, which are controlled by people who log into the MUD.

Because of this, it's often a good idea to store your players in a place separate from the rest of the characters in the game. I have opted to place them all in /data/players.

The database only allows you to save players, not every character. It's actually the region database, which you'll see later, that saves non-player characters to disk.

The SavePlayers and LoadPlayers functions are almost equivalent to the Save and Load functions from the account database; they just load character files instead of account files. Because of this, I'm not going to show you their code (how much stream code can a person take anyway?).

The other functions simply wrap around existing databases or functions, so I won't bother showing those to you either; you've seen similar code a hundred times already.

The two LoadTemplates functions are designed to load character templates from disk. The function that doesn't take any parameters loads up the manifest file found in /data/templates/characters, and loads character templates from every file located within the manifest.

The function that takes a string parameter loads a specific file; for example if you passed in playercharacters.data , the function would try to load all character templates from /data/ templates/characters/playercharacters.data.

Item Database

The item database function is extremely simple as well:

 class ItemDatabase : public TemplateInstanceDatabase<Item, ItemTemplate> { public:     void LoadTemplates();     void LoadTemplates( const std::string& p_file ); }; extern ItemDatabase ItemDB; 

Each of these functions simply wrap around the VectorDatabase functions, LoadDirectory and LoadFile functions, that are rerouted to load from the /data/templates/items directory. These two functions perform the same way as the LoadTemplates functions from the character database.

Room and Portal Databases

The room and portal databases are among the simplest database classes in the BetterMUD. Observe:

 class RoomDatabase : public VectorDatabase<Room> {}; extern RoomDatabase RoomDB; class PortalDatabase : public VectorDatabase<Portal> {}; extern PortalDatabase PortalDB; 

They're empty classes! That's it! I don't need to add any extra functions at all. This is because all the work of loading and saving rooms and portals is done through the region database class.

Region Database

The region database is the most complex of all the databases in terms of what it adds to the base database classes. Here's the class skeleton:

 class RegionDatabase : public VectorDatabase<Region> { public:     void LoadAll();     void LoadRegion( const std::string& p_name );     void SaveRegion( entityid p_region );     void SaveAll(); }; extern RegionDatabase RegionDB; 

Using this class, you can load every region in the game, load a specific region, save a specific region, or save every region in the game.

Every region in the game is stored within a directory of its own, and these directories contain five files: region.data, rooms.data, portals.data, characters.data, and items.data. As you might have guessed, these files store information about the region and all the rooms, portals, non-player characters, and items in the region.

Loading from a Manifest

In the game directory /data/regions, there is a manifest file, which should contain the names of every region in the game. For example, you might have a manifest file that looks like this:

 Betterton DwarvenMines ElvenForest 

With this manifest file, the game tries to load regions from the directories /data/regions/ Betterton, /data/regions/DwarvenMines, and /data/regions/ElvenForest. This function loads each of those names from the manifest and loads each region independently:

 void RegionDatabase::LoadAll() {     static std::string dir = "data/regions/manifest";     std::ifstream manifest( dir.c_str(), std::ios::binary );     manifest >> std::ws;     std::string regionname;     while( manifest.good() ) {         manifest >> regionname;         LoadRegion( regionname );     } } 

As you can see, the function simply invokes the LoadRegion helper function.

Loading a Specific Region

The database loads specific regions from disk using the LoadRegion function. You simply pass the name of the region you want to load, such as LoadRegion( "Betterton" ) , and the function tries loading that region from /data/regions/Betterton.

Here's the code:

 void RegionDatabase::LoadRegion( const std::string& p_name ) {     std::string dir = "data/regions/" + p_name + "/";     std::string regionfilename = dir + "region.data";     std::ifstream regionfile( regionfilename.c_str(), std::ios::binary );     Region &reg = LoadEntity( regionfile );     // load region from file     reg.SetDiskname( p_name );                  // set its disk name     // now load each individual component:     RoomDB.LoadFile( dir + "rooms" );     PortalDB.LoadFile( dir + "portals" );     CharacterDB.LoadFile( dir + "characters" );     ItemDB.LoadFile( dir + "items" ); } 

The function first calculates the name of the directory you're loading, and then the name of the region's datafile. Once that is finished, the region is loaded using LoadEntity (inherited from the Database classisn't that neat, and the disk name of the region is set, so that it knows how to save the region back to disk.

After that, the function manually invokes the LoadFile helper for the room, portal, character, and item databases for each file in the directory (for example rooms.data, portals.data). You don't need to add .data to the end of the filenames, because the Database::LoadFile function does that for you automatically.

That's all you need to load a region!

Saving a Specific Region

Saving a specific region to disk is slightly more complicated, mostly because the database classes don't have any kind of SaveFile function. Instead, you need to manually stream everything you need back to the files you need them in.

I'm going to split this function up to explain it better:

 void RegionDatabase::SaveRegion( entityid p_region ) {     Region& reg = get( p_region );     std::string workingdir = "data/regions/" + reg.Diskname();     std::string regionfilename = workingdir + "/region.data";     std::ofstream regionfile( regionfilename.c_str(), std::ios::binary );     SaveEntity( regionfile, reg ); 

The preceding code gets the region you want to save, and then constructs a working directory string, which is simply data/regions/ added to the disk name of a region. Databases

Once the string is constructed , the function opens up the "region.data" file in that directory, and streams the region right into it, overwriting whatever was there.

The code continues and saves the rooms of a region to disk:

 std::string roomsfilename = workingdir + "/rooms.data";     std::ofstream roomsfile( roomsfilename.c_str(), std::ios::binary );     Region::rooms::iterator ritr = reg.RoomsBegin();     while( ritr != reg.RoomsEnd() ) {         Room& r = RoomDB.get( *ritr );      // get room         RoomDB.SaveEntity( roomsfile, r );  // save it         roomsfile << "\n";                  // add newline         ++ritr;                             // go to next     } 

Saving the portals is a very similar process:

 std::string portalsfilename = workingdir + "/portals.data";     std::ofstream portalsfile( portalsfilename.c_str(), std::ios::binary );     Region::portals::iterator pitr = reg.PortalsBegin();     while( pitr != reg.PortalsEnd() ) {         Portal& p = PortalDB.get( *pitr );         PortalDB.SaveEntity( portalsfile, p );         portalsfile << "\n";         ++pitr;     } 

However, saving characters is a little different. Remember that characters are saved to their own files inside of the /data/players directory, so this directory needs to store only non-player characters (NPCs), which requires an if-statement:

 std::string charactersfilename = workingdir + "/characters.data";     std::ofstream charactersfile( charactersfilename.c_str(),                                   std::ios::binary );     Region::characters::iterator citr = reg.CharactersBegin();     while( citr != reg.CharactersEnd() ) {         Character& c = CharacterDB.get( *citr );         if( !c.IsPlayer() ) {           // only save non-players             CharacterDB.SaveEntity( charactersfile, c );             charactersfile << "\n";         }         ++citr;     } 

And finally, items are saved by using a process similar to rooms and portals:

 std::string itemsfilename = workingdir + "/items.data";     std::ofstream itemsfile( itemsfilename.c_str(), std::ios::binary );     Region::items::iterator iitr = reg.ItemsBegin();     while( iitr != reg.ItemsEnd() )     {         Item& i = ItemDB.get( *iitr );         ItemDB.SaveEntity( itemsfile, i );         itemsfile << "\n";         ++iitr;     } } 

That's the function that takes up the most space.

NOTE

You can clearly see that the blocks to save entities are very similar, and this should be setting off a warning bell in your head. You may consider writing some sort of templated function to perform this kind of a task in the future, if you ever plan on expanding the game. I just want you to know that the whole database system is a quick and dirty system, and you should by no means spend all your time studying it. My main focus in the BetterMUD is the separation of logic and data. I may one day implement a true database engine in the BetterMUD, but this will have to do for now.

Saving the Whole Thing

The final function in the region database is saving all the databases to disk:

 void RegionDatabase::SaveAll() {     iterator itr = m_container.begin();     while( itr != m_container.end() ) {         if( itr->ID() != 0 ) {             SaveRegion( itr->ID() );             ++itr;         }     } } 

The function essentially loops through all the regions (checking to make sure none of them has an ID of 0), and calls the SaveRegion helper to save that region to disk.

Directory Structure

The directory structure for the data of the BetterMUD is hierarchical; I've put everything into directories that make sense. All the data for the game is stored within the directory /data, which has many subdirectories, as shown by Figure 13.2.

Figure 13.2. The general directory structure of the BetterMUD.

graphic/13fig02.gif


Accounts and players are all stashed in the /data/accounts and /data/players directories. Regions, of course, are another level, in which each region has its own directory. The figure shows two regions, Betterton and the Dwarven Mines.

I haven't covered the /data/commands or /data/logics directories yet, but these two directories store scripts designed to be used within the game. Timers keep a listing of all timers in the game (see Chapter 15), logon keeps all the logon- related text files and scripts (see Chapter 16), and /data/templates stores all the item and character template files.

[ 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