Overall Module Design

[ LiB ]

The game module for the BetterMUD is more complex but still resembles certain aspects of the game module from the SimpleMUD.

Comparing the SimpleMUD Game to the BetterMUD Game

The first item of note is the name . Since I use namespaces for everything, I'm not really concerned about calling the class Game , because it's unlikely that the BetterMUD is going to have a different class with the same name inside it.

Another similarity is that the module keeps a timer (what would a MUD be without a timer, so that you can brag about how long you've been online?), and the module also loads and saves the databases to disk.

That's where the similarities end, however. In addition to the functions in the SimpleMUD that I've just mentioned, the Game class was responsible for the following:

  • Translating connection input into game commands

  • Performing the game commands

  • Responding to user commands

  • Sending data back directly to connections

Within the BetterMUD, all these topics are handled in other areas. For example, the BetterMUD now has a specific class called the TelnetGame class (see Chapter 16) that is designed only to translate connection input into game commands. You'll see this in use in the networking chapter, and understand how the design allows all the protocols you want to add.

Once the SimpleMUD knew what command a player wanted to execute, it figured out how to execute the command and did whatever was necessary to carry out the player's wishes. The BetterMUD's more flexible approach makes it irrelevant for the game module to know how user commands are implemented. Instead, as you learned in Chapter 14, there are command objects that take care of finding out how to do those things. This way, the game module doesn't need to know about player commands at all.

Sending back responses is an indirect process in the BetterMUD. The only thing the game module does is send event notifications back to entities, and it's up to the entities to consult their logic modules and figure out how they're going to send the data back to the player's connection. This is called the reporter concept, which is explained in detail within Chapter 16.

Game Module Functions

The BetterMUD game module is in charge of many new things, such as a timer registry, and keeping track of which players and characters are in the game. You can also use the game module to search for characters that are currently logged in or offline.

By and large, the biggest part of the game module is the physical entity management and action management. Let me go over the simple parts first.

So Little Time

As I've mentioned before, the game module keeps a timer, which you may access to get the number of milliseconds that the MUD has been running. The game module also has an elaborate timer system, which enables you to add actions, and run them at a later time. I've mentioned this concept before a few times, but now I'm finally showing it to you.

Timer

The timer functions are very simple:

 BasicLib::sint64 GetTime(); void ResetTime(); 

You're allowed to reset the timer if you wish, but I don't really recommend doing that because if your game is running, and it has a few hundred timer actions set to go off at time X, and you reset the time to 0, all those actions are going to execute sometime way in the future, quite possibly when they shouldn't be executed.

Timer Actions

In the previous chapter, I introduced you to the idea of actions, which are the primary methods of communication between the game and modules. The Action class is simple, yet very flexible, since it allows for a string data argument. The string data argument can contain almost anything you want, as long as whoever is receiving the action knows how to decode it.

When I showed you the HasLogic mixin class in Chapter 12, you saw that this class has a function called DoAction , which, when an action is passed in, tries to act based on that action.

While this method may not seem limiting, it is. Let's think about the actual game play for a moment. Imagine, once again, that you're a famous adventuring archeologist, breaking into a booby-trapped tomb to steal a priceless treasure; the moment you grab the treasure , the game starts a bunch of traps in motion.

Rather than going with the "instant death by dart-in-the-neck" route, you might want to toy with the hapless adventurer first; let him have the illusion that he can get out, instead of just killing him off right away. So this tomb could be a maze of sorts, with only one exit, and the moment the adventurer grabs the treasure, it sets off a trap that would cause the exit to close, but not right away. Instead, you want it to close after about 30 seconds or so, to give the adventurer a chance to actually get out.

This is where the timer actions come in. Instead of telling the game, "Do this now!" you can tell it, "Do this in 30 seconds!" instead. This adds a great deal of flexibility to your game.

Think about it in terms of illness and disease too; perhaps a player is poisoned by weak venom that disperses in an hour . You'd like the script that poisoned the player to also remove the poison , right? So the script executes two actions:

  • Tell the game to poison the character now

  • Tell the game to remove poison from the character in 60 minutes

To accomplish this, I've created the TimedAction structure (located within BetterMUD/ entites/Action.h):

 struct TimedAction {     BasicLib::sint64 executiontime;     Action actionevent;     bool valid;     void Hook();     void Unhook();     void Save( std::ofstream& p_stream );     void Load( std::ifstream& p_stream ); }; 

I've removed the constructors to make it look neater. For now, just look at the three pieces of data; I'll get to the functions in the next section.

The structure essentially maintains a time at which it should be executed, an Action structure, and a Boolean that determines if the timer action is still valid. You can simply think of these as actions that are executed at a given time.

Problems with Timer Actions

At this point, I'm going to discuss a concept that I haven't covered fully before, and it's rather difficult to understand at first. Let me explain by using an examplelet's go back to the poisoning example.

Example of Poisoning

Let us assume that your character is fighting an evil mutant python snake, and the snake bites the character, injecting a venom into his bloodstream. The character quickly kills the snake, but, alas, he's poisoned and is weakening by the minute.

Since being poisoned is unappealing (at least to most of us), your character wants to be cured as fast as possible. To do this, he'll seek out the local Medicine Man and purchase a venom cure. Congratulations! The character's now cured.

NOTE

Pythons aren't actually venomous. And now you know.

That whole process took maybe 45 minutes, and the character is eager to get back to fighting those evil snakes , so he goes deep into the forest, takes out his sword, and begins looking for another evil python to kill! OUCH!

It isn't his fault, but the character is not a very good warrior , and he's practicing and trying to improve, but those damn pythons are just too tough for him. While he was searching for another python, one slithered up behind him and poisoned him, again! Argh!

Now the character has to go all the way back to town , since, in a fit of impatience, he neglected to buy another venom cure. Or maybe he didn't have enough money; it doesn't matter. The point is that he's walking back to town, and halfway there the effects of the poison suddenly, and quite unexpectedly, disappear. He's cured!

Aftermath

The character is thinking, "What the heck?" The poison is supposed to last 60 minutes, yet he was poisoned only 10 minutes ago! While this isn't something you would normally complain about (Look Ma! Free cure!), as a programmer, it should be tingling that little sense in the back of your mind that keeps repeating, "This is wrong, it's not working the way it should!"

Oh boy. Why would something like this happen? The answer has to do with the timer system. First look at Figure 15.2, in which timer events are shaded. The original "remove poison" timer event was never removed when the poison was mitigated.

Figure 15.2. The process of being poisoned, cured, and then poisoned again.

graphic/15fig02.gif


Can you see the problem? When a character is first poisoned, the snake poison script automatically sets a timer event for removing the poison 60 minutes from the time of poisoning. In the meantime, the character goes to buy a poison cure, is cured, is poisoned again, but then the poison unexpectedly disappears, since the original timer event still exists. In addition, if your character finally gives up trying to kill those damned pythons, within 45 minutes or so, the game is going to try removing the poison again, but this time, the player isn't poisoned, so the game will be trying to remove a logic script that doesn't exist.

D'oh!

When Things Go from Bad to Worse

Guess what! It gets even worse. Imagine a script that tells a computer character to perform a specific action at a specific time; but before he has the chance to do so, another character comes by and rudely interrupts his life with a sword to the gut. The poor guy dies, but the game still wants to tell him to do something later on.

What can happen? The best-case scenario is that your game, when getting the character from the database, notices that he doesn't exist anymore. This causes your database to throw an exception at you, but that's a good thing. When an exception is thrown, the game catches it, and gives up trying; it says, "Yeah, you know what, this ain't gonna work," and it tosses the action event away, doing no harm at all.

What's the problem then? The problem is that the game re- assigns ID numbers to entities. Imagine, that somewhere else in the game, a random new character receives the previous character's ID. When the game reaches the action, it performs the action on the new charac ter! That's a major "OOPS!" right there. Figure 15.3 demonstrates this.

Figure 15.3. This figure shows the two possible outcomes of calling a timed action on a character that doesn't exist anymore.

graphic/15fig03.gif


Actions should never ever ever ever ever accidentally switch ownership. As far as the game is concerned, when that poor guy dies, every action he's attached to should die as well.

Hooks

So, now what? How do we make a system that links an entity to a timer action? It turns out that this system is actually pretty simple. The timer actions already know which entities they rely on by their values. For example, an "attemptgiveitem" action relies on three volatile entities: the person giving an item, the person getting an item, and the actual item.

For this to work, all three entities must exist; if any of them stops existing, that's itthe timer action cannot possibly work in a meaningful way, so it should be invalidated.

Since timer actions already know who their actors are, half the work is already done. The other part, of course, is making sure that entities know which timer objects they are attached to. This is where the term hook comes from. Essentially the entities are hooked onto the timer action, and when they die, they tell the timer action, "Hey! I died! You're no longer valid!" Then, of course, the timer object has to go to every entity it's attached to and remove itself.

Figure 15.4 shows an example of three entities linked with three timed actions in various ways. All entities have hooks into timed actions that they control.

Figure 15.4. Hard pointers ( TimedAction* ) and soft pointers ( entityid lookups) combine to create the BetterMUD hooking system.

graphic/15fig04.gif


When I showed you the LogicEntity mixin class in Chapter 12, I briefly told you about the hook functions it contained, but I didn't elaborate. Here is a listing of them:

 typedef std::set<TimedAction*> actionhooks; typedef actionhooks::iterator hookitr; void AddHook( TimedAction* p_hook ); void DelHook( TimedAction* p_hook ); hookitr HooksBegin(); hookitr HooksEnd(); size_t Hooks(); void ClearHooks(); void ClearLogicHooks( const std::string& p_logic ); void KillHook( const std::string& p_act, const std::string& p_stringdata ); 

Remember, these functions are automatically included in any entity class that inherits from the LogicEntity mixin class, so you can give your entities hook capabilities quite easily.

The first two lines define typedefs that make your life easier; actionhooks is simply a set of TimedAction pointers.

The next two lines are the AddHook and DelHook functions. Whenever a new timed action is added to the game, it automatically contacts every entity that it interacts with, and tells it to add a pointer to the action to its hooks. When the timed action is removed from the game, it contacts each of its entities and removes itself from the entity's set of hooks. Since these two functions simply wrap around the std::set::insert and erase functions, there's no point in pasting in the code here.

NOTE

Figure 15.4 should illustrate why I generally like to stay away from pointers; they can become a huge mess very quickly. There are better ways of going about a hooking system, such as using smart refer ence classes. Indeed, if I had made the entire timer system in Python, I could have used the built-in weakref class to do such a thing. You should look into using weak references if you plan on doing more with Python.

The other two functions of interest are the ClearHooks and ClearLogicHooks functions.

Clearing All Hooks

Whenever the game destroys an entity, it calls its ClearHooks function, which basically goes through every timed action hook in the entity, and tells it to unhook itself. It's just a simple loop:

NOTE

Heaps, which I also use in this chapter (hooray!), are my favorite data structure. Sets are my second favorite. As I've told you many times already, a set is a cool structure to use when you want fast insertions, lookups, and deletions. The coolest thing about sets is that they work just fine with pointers too.

 void ClearHooks() {     hookitr itr = HooksBegin();     while( itr != HooksEnd() ) {         hookitr current = itr++;         (*current)->Unhook();     } } 

There's one little catch, however. You need to maintain two iterators at all times, since the timed action's Unhook function actually deletes the object that the current iterator points to. So if you do this

 (*itr)->Unhook() ++itr 

your code is likely to crash, since the object the iterator points to doesn't exist anymore.

Clearing Logic Hooks

Whenever the game removes a logic module from an entity, any timed events that were specifically intended to go to that logic module must be deleted. You learned in the previous chapter that there are three events associated with logic modules: addlogic , dellogic , and messagelogic . As it turns out, only two of these actions must be hooked: dellogic and messagelogic .

The adding of logic modules doesn't need to be hooked, because it is assumed that the logic module doesn't already exist, and you can't hook to a nonexistent object, now can you?

On the other hand, messages or logic deletion actions must be told if the logic module suddenly disappears (for example when a character gets a venom cure from a shaman). The player hasn't disappeared at all, but the logic module that the timer needs has disappeared.

You could let the logic modules themselves hold containers of hooks, but as you saw from the previous chapter, I don't do that. There are a bunch of issues to consider, such as needing entities and their logic modules to keep pointers to the timer. While this doesn't produce substantial overhead, it does involve extra management and additional code. Another issue is creation time; you will often want to delay the installation of a logic module onto a character by a few minutes, but also set the removal time (give a character the logic in 10 minutes, and remove it in 20). There's a slight problem, however. If you do that, when you put the closing time into the game, the logic module you want closed doesn't even exist yet! Obviously there's no way to hook that, now is there?

So, instead of hooking the action to a logic module, I hook it to the entity in question instead, and whenever a logic module disappears, I call the ClearLogicHooks function. This function is rather tricky, because you need to search through all the entity's hooks, and find any that are directed toward the module you just deleted:

 void ClearLogicHooks( const std::string& p_logic ) {     hookitr itr = m_hooks.begin();     while( itr != m_hooks.end() ) {         hookitr current = itr++;         if( (*current)->actionevent.actiontype == "messagelogic"              (*current)->actionevent.actiontype == "dellogic"  ) {             if( ParseWord( (*current)->actionevent.stringdata, 0 )                 == p_logic )                 (*current)->Unhook();         }     } } 

The code loops through (keeping track of the next iterator, just like the ClearHooks function), and if you find any messagelogic or dellogic actions, you know they're hooked to the current entity, but you don't know if they point the logic module you're deleting. So you need to call the BasicLib::ParseWord function and extract the name of the target module from the action's stringdata field.

If you determine that the action was intended for the module you're unhooking, you can tell the timer action to unhook itself.

NOTE

Remember from the last chapter that when sending messages to modules using the "messagelogic" action, the name of the module is stored in stringdata , but you can also store more string data after the name. This means that the name of the target module is the first word of the string, and that anything after that is just extra data to be inter preted by the module, so the ClearHooks function doesn't need it.

Killing Arbitrary Hooks

At times, you'll need a way to kill a hook manually. For example, you're probably going to want to kill actions like "repeating poison damage" for players when they log off, so that they don't die while they're not even logged in.

To do this, you need to be able to find a hook to an action and kill it manually. For this purpose, I've created the KillHook function, which searches for an action based on its type and its data string, and then kills it:

 void KillHook( const std::string& p_act, const std::string& p_stringdata ) {     using BasicLib::ParseWord;     hookitr itr = HooksBegin();     while( itr != HooksEnd() ) {         hookitr current = itr++;         // unhook the event if it matches the parameters         if( (*current)->actionevent.actiontype == p_act &&             ParseWord( (*current)->actionevent.stringdata, 0 ) ==             p_stringdata )             (*current)->Unhook();     } } 

This functions loops through all the hooks and makes sure the action name and the first word of the data string match. For example, in a poison script, you might have a timed action with the action name do and the action string repeatpoison , so to kill that action you merely call the function like this:

 KillHook( "do", "repeatpoison" ); 

The reason it compares only the first word of the data string is because you can add extra string parameters to the data string; say for example you want to tell the script how much to poison someone (for example, a damage level of 10), you could create the action using "repeatpoison 10" as the data string. Whoever is killing the action probably won't care about the extra parameter, so it doesn't bother comparing anything past the first word.

Hooking and Unhooking Timer Actions

From the previous section, you saw that making an entity unhook itself was a relatively painless processall you need to do is call the Unhook function of timed action objects. Of course, before you begin to do any unhooking of the timed actions, you need to first hook them to their entities.

Hook Function

When you first create a new timed action object, you need to make it find every entity it is attached to and connect them together. This is accomplished with the TimedAction::Hook function, which I will show you only part of:

 void TimedAction::Hook() {     if( actionevent.actiontype == "attemptsay"          actionevent.actiontype == "command"          actionevent.actiontype == "attemptenterportal"          actionevent.actiontype == "attempttransport"          actionevent.actiontype == "transport"          actionevent.actiontype == "destroycharacter" ) {         character( actionevent.data1 ).AddHook( this );     }     else if( actionevent.actiontype == "attemptgetitem"               actionevent.actiontype == "attemptdropitem" ) {         character( actionevent.data1 ).AddHook( this );         item( actionevent.data2 ).AddHook( this );     } 

This shows the function hooking up seven different types of actions; attempting to say something, sending a command, attempting to enter a portal, attempting to transport, forcing a transport, destroying a character, and attempting to get and drop items.

If you compare this code to the tables in the previous chapter, you'll see that those first five actions are the actions that use data1 as a character pointer and don't rely on any other entities.

The next two actions are the two actions that use data1 as a character, and data2 as an item.

From the code, you can see that they just add hooks to the required entities.

There's one other special case in the code: the logic actions. Here's the code for them:

 else if( actionevent.actiontype == "messagelogic"               actionevent.actiontype == "dellogic" ) {         switch( actionevent.data1 ) {         case CHARACTER:             character( actionevent.data2 ).AddHook( this );             break;         case ITEM:             item( actionevent.data2 ).AddHook( this );             break;         case ROOM:             room( actionevent.data2 ).AddHook( this );             break;         case PORTAL:             portal( actionevent.data2 ).AddHook( this );             break;         case REGION:             region( actionevent.data2 ).AddHook( this );             break;         }     } 

Remember from the last chapter that data1 represents the type of the entity whose logic module you are acting on0 means a character, 1 an item, and so on. I've made an enumerated type that wraps around those values, so you can read the code more easily (that is, CHARACTER is an enum with a value of 0, and so on). The function determines what kind of entity you want to act on, and then enters a switch statement to add the hooks where needed.

The function has other cases to handle other kinds of actions, but they're all in a similar vein, so I don't show them here.

NOTE

In the code, I used unnamed temp- oraries objects that are constructed , called, and then immediately dis carded. I did this by calling the constructor character( id ) , and then adding the function call after that. It's just a trick used to make the code look a little cleaner; since I don't need to look the entities up anywhere else, it's acceptable.

Unhook Function

What is hooked must eventually be unhooked, so there is also an Unhook function. It's very similar to the hook function, but it works the other way around. Here's a sample:

 void TimedAction::Unhook() {     valid = false;     if( actionevent.actiontype == "attemptsay"          actionevent.actiontype == "command"          actionevent.actiontype == "attemptenterportal"          actionevent.actiontype == "attempttransport"          actionevent.actiontype == "transport"          actionevent.actiontype == "destroycharacter" ) {         character( actionevent.data1 ).DelHook( this );     } 

The first part of the code sets the valid variable to false once you unhook a timer action, it is assumed to be invalid and shouldn't be executed.

The next part of code looks very similar to the code I showed you before, but instead of hooking the timer action... you guessed it! It's unhooked instead!

Timer Registry

Now that you have timer actions, a timer object, and the ability to hook actions to entities, you need some way of actually storing these timer action events in the game. I have touched on this topic before, and I mentioned using a priority queue to store timer actions, which is by far the best method.

NOTE

Here's a little something to think about for the future. The action format, while somewhat flexible, is ulti mately limited to four entity slots, which have variable meanings depending on the type of action. Because of this setup, you end up with the code shown hereit checks the name of every action to determine what kind of actors it has. In the future, you might want to consider an even more flexible format that will allow you to determine what kind of entity a field represents, instead of making the field meanings depend on a pre- coded action. This method allows you to add an infinite number of actions to the gameall with automatic hooking support. Oh well, you can't have everything. I'll probably implement something like this if I ever create a BetterMUD 2.0; or maybe I'll call it BestMUD.

A Queue for You

Let me go over the concept again. At one point in time, the game may have as many as a few dozen to a few thousand timer actions in memory (quite possibly more, if you wish). If you store all these actions in a list, you have to go through the list every time the game loop executes, and ask every single one of them, "Hey, can you execute yet?"

If you're an experienced programmer, there should be a little voice in your head saying, " No! No! Bad programmer! No cookie for you!" Going through every timer action in the game once per game loop is incredibly dumb and wasteful . This wastes so much time it's not even funny . There are other solutions, of course, such as checking the timers only once every second as the SimpleMUD did. That's a bad idea, too; you don't allow your game much flexibility by forcing actions to occur on one-second boundaries.

Think about something though; if you have an action that occurs an hour from now, why would you check it every loop from now until an hour from now? If you have an action that happens in 10 minutes, you know that the one that happens in 60 minutes happens after it. So you think, "Hey, why don't I check to see if the 10-minute action can be executed yet, and when it finally can, I'll start checking the 60-minute action."

Now here comes the priority queue. A priority queue (the STL implementation of priority_queue uses a special binary tree called a heap ) is a queue that automatically arranges items by their number, and puts the item with the highest priority at the top. The game module has a priority queue for timer actions:

 typedef std::priority_queue<     TimedAction*,                         // datatype     std::vector<TimedAction*>,            // container type     TimedActionComp >                     // comparison type     timerqueue;                           // typedef name timerqueue m_timerregistry; 

One thing about STL is that it's very flexible. Of course, that flexibility comes at a price ugly code. I almost laughed when I made the definition for a timer queue... that is one nasty piece of code right there.

Meaning of the Code

Let me break the priority queue code down for you. The first part of the code, std::priority_queue , obviously represents the STL heap data structure. I want this heap to store TimedAction* s, so that's used as the first template parameter.

The next template parameter is std::vector<TimedAction*> , which tells the queue to use a vector of TimedAction* as its underlying container. The priority_queue class is an adaptor , meaning that it only defines how to work on a container, not the actual container itself. So, inside of the queue, I've told it to use a vector of action pointers. You could theoretically use any random access container you want (STL has one other, the deque ), but it's just easier and faster to use a vector.

The final template parameter is a comparison functor object, which sorts the timer actions inside the queue. For this, I needed to create my own custom TimedActionComp functor:

 class TimedActionComp :     public std::binary_function<TimedAction*, TimedAction*, bool> { public:     bool operator()( const first_argument_type& left,                    const second_argument_type& right ) {         return left->executiontime > right->executiontime;     } }; 

Ack! More ugly code! Unfortunately, you'll have to get used to this when working extensively with the STL. Basically, the function inherits from std::binary_function , which is a base class defined by STL that defines a functor that takes two parameters for operator() , and returns a specific type. For example, I used parameters <TimedAction*, TimedAction*, bool> , meaning that the functor's operator() takes two TimedAction* s as parameters, and returns a Boolean.

Here's an example of its use:

 TimedActionComp comparetimes; TimedAction* action1 = // whatever TimedAction* action2 = // whatever bool b = comaparetimes( action1, action2 ); 

The functor object essentially compares the two timed action pointers and returns a Bool-ean based on the function defined inside the functor.

If you look back at the functor definition, you can see that the function returns true whenever the execution time of the left parameter is greater than the execution time of the right parameter; essentially the function returns true when the left action occurs at a later time than the right.

If you don't pass in a custom functor comparison object, the std::priority_queue class uses, by default, std::less<TimedAction*>. The std::less functor is an object that returns true if the left TimedAction* is less than the right TimedAction* . Of course, this is not what we want to do, since the function will be comparing pointer values, so that anything that is at a higher memory address will end up at the top of the queue.

So the TimedActionComp class compares two pointers of timed action object and makes it look as if any actions that occur at a later time are less than objects that occur at an earlier time.

Eventually this means that the action that must be executed next is always at the top of the priority queue, and anything else below it is executed at a later time. This means that for any given game loop, you only need to check one timer object, and if it isn't time for that top object to execute yet, you know it's not time for any other objects to execute yet!

Adding Timed Actions

Now whenever you need to add a new timed action to the game, all you need to do is call the AddTimedAction class with a pointer to a new timed action, and the game manages that action from now on.

Here's the function:

 void Game::AddTimedAction( TimedAction* p_action ) {     m_timerregistry.push( p_action );     p_action->Hook(); } 

The function pushes the new action onto the registry, and then hooks the action to all its actors. You should always pass new pointers to timed actions into this function; once the game module has control over a timed object, it takes care of the object and deletes it automatically when it's finished with it.

To Be or Not to Be

Earlier I told you all about hooks, and how entities need to tell the timed actions that they are hooked into to kill themselves when the entity itself dies. But there's a problem: you can't remove timers from the timer registry. The std::priority_queue class has absolutely no way to remove anything from the queue except the very top item, so deleting the timer action when it gets unhooked is almost impossible . Sure, you might be able to extract everything, discard the deleted action, and then re-insert everything, but that is just an incredible mess, and a waste of time to boot.

This is where the valid field of a TimedAction becomes useful. Whenever you tell an action to unhook itself, that means it's no longer a valid action, so the valid Boolean is set to false . Later on, when the game loop gets around to executing that action, it sees that it's no longer valid, so it won't bother executing the action; it deletes it instead.

Executing Timed Actions

The final part of the timer system is actually executing the actions, which is a very simple process:

 void Game::ExecuteLoop() {     BasicLib::sint64 t = GetTime();        // get time     while( m_timerregistry.size() > 0 &&   // loop while there are timers            m_timerregistry.top()->executiontime <= t ) {  // is it time yet?         TimedAction* a = m_timerregistry.top();  // get the action         m_timerregistry.pop();                   // pop off the action         if( a->valid ) {                         // make sure it's valid             try {             a->Unhook();                        // unhook it             DoAction( a->actionevent );         // perform the action             }   catch( ... ) {}         }         delete a;                                // delete the action     } } 

The function starts by retrieving the current system time, so that it doesn't have to perform multiple time lookups, which are usually expensive operations (time lookups typically use a hardware device that isn't attached to the CPU directly).

The while-loop loops through the timer registry while there are timers available (it could be empty), and the top timer has an execution time that is less than or equal to the current game time.

If an action needs to be executed, a pointer is retrieved from the top of the queue, and the pointer is popped off the queue. From this point forward, the game assumes that the action has been handled, so the queue no longer needs to keep a reference to it.

Now the action is checked to see if it's valid, and if so, the action is unhooked and executed.

Once the execution is completed, the timer action itself is deleted, and the loop continues.

The ExecuteLoop function will be called once per loop inside the main function, which I'll get to later in this chapter.

NOTE

Dealing with dynamic memory here is a very risky business. Either the Unhook or the DoAction functions may throw exceptions, and if they do, you automatically lose a pointer to a , meaning you've got a TimedAction object floating in memory with nothing pointing to it. This can be a big problem, since the game should keep running and actions will fail to perform, and thus the game catches exceptions and keeps running. If you have enough thrown exceptions while performing timer actions (through faulty scripts, or whatever), you'll end up with a large memory leak. That's why whenever an excep tion is thrown, it is caught.

Timed Action Helpers

You shouldn't really be creating new TimedAction objects on your own and passing them into AddTimedAction . Instead, you should rely on the four helper functions I've provided:

 void AddActionRelative( BasicLib::sint64 p_time, const Action& p_action ); void AddActionAbsolute( BasicLib::sint64 p_time, const Action& p_action ); void AddActionRelative(     BasicLib::sint64 p_time,     const std::string& p_act,     entityid p_data1 = 0,     entityid p_data2 = 0,     entityid p_data3 = 0,     entityid p_data4 = 0,     const std::string& p_data = "" ); void AddActionAbsolute(     BasicLib::sint64 p_time,     const std::string& p_act,     entityid p_data1 = 0,     entityid p_data2 = 0,     entityid p_data3 = 0,     entityid p_data4 = 0,     const std::string& p_data = "" ); 

The first two functions allow passing in an action object directly, and the second two allow passing in a variable number of parameters, which will be used to create a new action.

Basically, the functions allow you to create an action at an absolute time or a relative time . An absolute time means that you're going to execute the action at a time relative to 0; if you specify AddActionAbsolute( 10000, act ) , you're telling the game to execute an action at exactly 10 seconds from the time the game first started. As you can imagine, this function is somewhat limiting, due to the fact that you really can't assume things are going to happen at any specific time.

On the other hand, you'll often want to "add an action, which will execute X minutes from now," and this is the purpose of the AddActionRelative functions. I'll show you the functions that take actual Action objects as their parameters:

 void AddActionRelative( BasicLib::sint64 p_time, const Action& p_action ) {     AddTimedAction( new TimedAction( p_time + GetTime(), p_action ) ); } void AddActionAbsolute( BasicLib::sint64 p_time, const Action& p_action ) {     AddTimedAction( new TimedAction( p_time, p_action ) ); } 

The relative version takes the time you passed in and adds the current time to it; the absolute version simply passes the time directly into a new TimedAction object.

Database Functions

The game module has a bunch of functions related to database management. They can be separated into a few categories.

Loading Functions

The first of the database management functions are the loading functions:

 void LoadAll(); void LoadTimers(); void ReloadItemTemplates( const std::string& p_file ); void ReloadCharacterTemplates( const std::string& p_file ); void ReloadRegion( const std::string& p_name ); void ReloadCommandScript(     const std::string& p_name,     SCRIPTRELOADMODE p_mode ); void ReloadLogicScript(     const std::string& p_name,     SCRIPTRELOADMODE p_mode ); 

Some of these functions are self explanatory, such as the LoadAll and LoadTimers functions, which respectively load everything from disk, or just the timers. The other functions load specific types of items from a given file; they wrap around the database classes, so I won't show you the function definitions. However, I will show you how to use them:

 ReloadItemTemplates( "newitems" ); ReloadCharacterTemplates( "newcharacters" ); ReloadRegion( "SomeArea" ); ReloadCommandScript( "newcommands" ); ReloadLogicScript( "characters.newcharacterlogic" ); 

The previous function calls load entities from the following files:

data/templates/items/newitems.data

data/templates/characters/newcharacters.data

data/ regions /SomeArea/

data/commands/newcommands.py

data/logics/characters/newcharacterlogic.py

The first two are simple; they simply load (or reload) all template entities within a specific file. The function automatically assumes it exists within the /data/templates/items or / data/templates/characters directory.

The region function loads all five standard region files (items.data, characters.data, region.data, rooms.data, and portals.data) from the directory /data/regions/SomeArea.

The last two are Python scripts, which you'll learn about in Chapters 17 and 18. Due to the way the logic databases are set up, you must specify what kind of logic you're loading, such as "character.logic" to load a character logic module, or "items.logic" to load an extra item logic.

Saving Functions

There are a few saving functions as well:

 void SaveAll(); void SavePlayers(); void SaveRegion( entityid p_region ); void SaveTimers(); 

These functions do the following:

Save the whole game to disk

Save all players to disk

Save a particular region to disk

Save all the timer objects to disk

Other Functions

The other function related to databases is the Cleanup function:

 void Cleanup(); 

This simply tells all the volatile databases (characters and items) to clean up any entities that have been deleted:

 void Game::Cleanup() {     ItemDB.Cleanup();     CharacterDB.Cleanup(); } 

You should remember database cleanups from Chapter 12.

Timer Disk Functions

When you shut down the MUD, there's a bunch of timed action objects in memoryevents that are important to the game, and thus must be written to disk. Otherwise, the next time the game loads, a number of things won't work properly, since you'll start up with no event objects.

The BetterMUD takes care of loading and saving timer objects by using the Load and Save functions of the TimedAction class. Let me show you the loader first:

 void Game::LoadTimers() {     std::ifstream timerfile( "data/timers/timers.data", std::ios::binary );     std::string temp;     timerfile >> std::ws;     if( timerfile.good() ) {     // make sure file exists         timerfile >> temp;         BasicLib::sint64 t;         BasicLib::extract( timerfile, t );   // load the time         m_gametime.Reset( t );               // set the time         timerfile >> std::ws;         while( timerfile.good() ) {          // load each timer now             TimedAction* a = new TimedAction;             a->Load( timerfile );             timerfile >> std::ws;             AddTimedAction( a );         }     } } 

The function tries to open up the file, and load in a game time, nearly as it did in the SimpleMUD.

Once the function does that, it creates new timed action objects, loads them from the disk, and sends them off to the timer registry.

The opposite of loading is saving, of course:

 void Game::SaveTimers() {     std::ofstream timerfile( "data/timers/timers.data", std::ios::binary );     timerfile << "[GAMETIME]              ";     BasicLib::insert( timerfile, GetTime() );     timerfile << "\n\n";     timerqueue temp( m_timerregistry );   // copy queue     while( temp.size() ) {                // go through each action         TimedAction* a = temp.top();         temp.pop();         a->Save( timerfile );         timerfile << "\n";     } } 

This time the function saves the game time out to data/timers/timers.data, and then it writes out everything in the timer event queue.

This is the tricky part, however. The only way to get the contents of a priority queue is to pop them all out and write them. This isn't a problem if you're shutting down the server, but eventually, you'll need to save the actions while the game is running. You could pop them all out, write them, and then push them all back into the queue, but that requires a lot of management. Instead of doing that, I create a copy of the queue, named temp , go through the copy, and pop everything out of it. Since I'm dealing with pointers, there's really no problem with abandoning the pointers.

NOTE

It doesn't really matter what superglue does, but if you're inter ested, I've included the script on the CD. It's a simple Python script that glues a user to a room for 20 sec onds, so that he can't move around. It's pretty funny.

So, the file that is used in conjunction with the timer's Load and Save functions would look something like this:

 [GAMETIME]              205076603 [TIMER]     [TIME]                  205094154     [NAME]                  messagelogic     [DATA1]                 0     [DATA2]                 1     [DATA3]                 0     [DATA4]                 0     [STRING]                superglue remove [/TIMER] 

This depicts a simple timer action trying to message a logic module that I used for testing: superglue!

[ 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