Actions


What is an action?

The term action means something that is scheduled to happen. Well why do we need to schedule action objects, why don't they just happen immediately? There are two main reasons.

First of all, there must be a delay before an action actually occurs. Partly because this makes the game more realistic (it should take time, although very little time, to pick up something from the ground). Delays also remove the risk of endless action-reaction loops stopping the whole game. Imagine a computer-controlled creature that automatically says "hi" whenever he hears anyone else say "hi". What happens if two of these guys are standing in the same room, and a player-controlled character walks in and says "hi". Endless loop, of course - the two schmucks will keep saying hi to each other for all eternity (well, at least as long as the server is running). Furthermore, all other activity in the world will freeze since none of the two characters will seize control or take a paus - they will just keep going as fast as the processor will allow them. With a slight delay the system (ie time itself) won't freeze up (although they will still stand around saying hi to each other for all of eternity).

The second reason for using action objects is that these can then be verified - certain actions might fail (I can't walk through a wall, for example). Actions might even (in certain cases) be modified. Let's say the character is drunk. He intended to take a step north, but he ended up taking a step west instead. Or he intended to say "Hi guys, what's up?" but it came out "Grblrfrthz... uh".

Actions and events

An Event is something that has happened. An Action is something that is executed, after a certain delay. An Action may result in one or more Events. If it doesn't then it was cancelled, ie invalid in way or another. This isn't the same as saying that the action failed, since a failed action will usually result in one or more Events also, although perhaps not the intended events...

Let's look at two examples of Actions and their corresponding events.

If Fred says hi, a "talk" action will be created. When executed it will fire a corresponding "talk" event.

If Fred walks north, a "move" action will be created. Let's say Fred is standing in the Garden and that "north" leads to the Pub. When executed a corresponding "move" event will be fired. Several additional events will be fired as well - Fred's location has changed so a "property change" event is generated. From the Garden's point of view, it's collection of items has been reduced, so it will fire an "item holder" event stating that Fred has exited. Finally, the Pub will fire an "item holder" event stating that Fred has entered.

In both examples above there is a directly corresponding event for each action. In addition, the second action generates a few indirect or secondary events. The secondary events cannot occur "on their own" - they need a triggering action. This means that the Action classes are a subset of the Event classes, ie each Action has a directly corresponding Event, but every event does not necessarily have a corresponding action. For this reason I have decided to use the following philosophy:

Direct vs indirect events

Each event class must be defined as either direct or indirect.  A direct event has two phases of it's life time, the action phase and the event phase. The action phase represents the time period before the event actually "occurs", ie when it is in the action queue waiting to be executed. The event phase is the time period after the event "occurs", ie when it is being propagated around the container hierarchy. Indirect events do not have an action phase, since they are the indirect result of direct events.

Hence a direct event is equivalent to an Action.  Here is the event class hierarchy.

Event states

During an event's lifetime it changes state.  An indirect event has only two states: local and remote. A local event is located on the server side.  Before sending it over to the client side, however, it must be turned into a remote event - this means all references to Items are converted to ItemHeaders, since Items may not be sent over to the client (click here to find out why).

Direct events can have up to four states:

  1. When it iscreated on the client side it is a "remote action" (from the server's point of view)
  2. When it has been sent to the server side and is waiting in the action queue it is a "local action"
  3. When it has been executed and is being propagated around in the server it is a "local event"
  4. When the event notification is sent to the client side it finally becomes a "remote event"

The lifetime of an action

When Fred wants to move north, a MoveDirectionEvent is created containing information on who wants to move and where he wants to go. A delay attribute will be added as well, depending on the physical attributes of Fred's body (ie how fast he is). This action will be given to the global Clock. The Clock will figure out when this action is due to occur (current time + delay, right?) and place it in the action queue. The actions in the action queue are executed in order as time progresses, and finally it is Fred's turn.

Now the clock will tell the action to execute (remember - MoveDirectionEvent is a subclass if DirectEvent and is therefore also considered to be an action). The MoveDirectionEvent will try to move Fred north, which may or may not succeed, depending on if there is anything blocking the way. Other factors such as physical health, intoxication, darkness, etc may also affect the actual outcome of the action.

As soon as the action is complete, a number of events will usually be generated. If the MoveDirectionEvent succeeded, three indirect events will be fired in addition to the original MoveDirectionEvent (see the event propagation document for details about this):

  1. The square that Fred is exiting will fire an ItemHolderEvent stating that Fred has exited
  2. The square that Fred is entering will fire an ItemHolderEvent stating that Fred has entered
  3. Fred will fire a PropertyEvent stating that his location property has changed value.

Action filtering

An action usually has 1 to 3 direct participants.  "say hello" has one participant (me), "drop sword" has two participants (me and the sword), and "put bottle in sack" has three participants (me, the bottle, and the sack).  There will also be any number of indirect participants such as the floor, the shoes worn by the attacker, etc.

When executing an action, each direct participant gets a chance to filter (modify) the result, or even cancel the action outright (the sack would cancel the action if the bottle couldn't fit).  Some of the indirect participants may also be given the chance to filter the action, depend on the nature (and complexity) of the action.    What is the difference between an Action and an Activity?  Well the main difference is that Actions are "instant", or "atomic".  Even though there is usually a small delay before the Action is executed, it doesn't have duration - once it is executed, it will disappear.  It won't stick around and do more things for you.  Activies are more like processes - Activities deliver actions and as such work kind of like souls - temporary souls.  See the seperate document on activities for more on this...

When executing a skill based action such as "attack wolf", the skill level of the attacker and defender is also considered.  The condition of the attacker's weapon is considered, perhaps the lighting in the room is also considered.  All in all, the outcome is determined by the combined effect of all participants (both direct and indirect). This is how it's implemented:

How actions are filtered

Let's say an AttackEvent is created and queued, just like any action.  The attacker and defender is specified, perhaps the attack strategies and weapons used as well.  When the action is executed, a preliminary result (ActionResult) is calculated using some kind of predefined algorithm or formula - some mix of the attacker's strength, skill, weapon type, the defender's dodging ability, etc.  The ActionResult is just a specification of what will happen when the action is complete (so nothing has happened yet).  Now here comes the nice part.

The AttackEvent will determine which participants are allowed to filter (or modify) this result, and each of them will be asked for an ActionResultFilter.  This is optional, most participants will probably accept the preliminary result.  But let's say the weapon used is an OrcTrasherBlade - extra effective for whomping Orcs.

When this weapon is asked to produce an ActionResultFilter it will check if the target was an Orc.  If so, it will return an ActionResultFilter (actually an AttackResultFilter, a subclass of ActionResultFilter) representing +3 damage.  One important thing about ActionResultFilters is that they can be combined, so all the ActionResultFilters produced by the participants will be but together into one single, combined filter.  This will then by applied on the preliminary action result, producing a final result.

The final result will then be executed, and the action will be completed. Here's a summary of what the action object does::

  1. Calculates a preliminary ActionResult using a hardcoded algorithm
  2. Asks each participant for an ActionResultFilter
  3. Puts together all the filters into one combined ActionResultFilter
  4. Apply the filter to the preliminary ActionResult, producing a final ActionResult
  5. Tell the ActionResult to execute itself.

Features of the ActionResultFilter

The main point of using ActionResultFilters is that they can be combined to produce a net effect.  It can also resolve complex issues such as this one.  Imagine if Fred attacks the orc with an OrcThrasherSword which is magical and can never miss an orc.  But the orc has a magic AntiHumanShield which means humans always miss when attacking!  Now what?

The good thing here is that this will be resolved in an "organized" fashion using the ActionResultFilters.  When two filters are combined, possible contradictions have to be resolved, so in this case it may be defined (in the combine(...) method of AttackResultFilter) that an "always miss" filter overrides an "always hit" filter - so combining these two will result in an "always miss" filter.  Or it may be defined that they cancel each other (much better in my opinion) so that it is treated as a "no effect" filter.  Weights can be introduced too, like the sword is more powerful than the shield, and stuff like that.


Henrik Kniberg

Last updated: