-
Notifications
You must be signed in to change notification settings - Fork 1
Dispatcher System
The Dispatcher system is at the core of the game's gameplay system.
It is essentially an Event system, which is also coupled to a state keeping system applied to each game character / object via Conditions.
Events are most often just queries performed by the engine to obtain a value, such as getting a character's To Hit bonus. To illustrate, the game engine might do something like this:
toHitBonus = Dispatch(obj, dispTypeToHitBonusBase, eventObj)
In this case, the Event Type (or Dispatch Type) is dispTypeToHitBonusBase.
This function call triggers an Event that tells the engine that game object obj is looking to get its Base To Hit Bonus (BAB). It is then up to the various Conditions applied to the character to fill in the bonuses, e.g. from its character class or a spell.
Let's take a look at the Barbarian condition for example.
Arguments: 0
Dispatcher Hooks:
Event Type | Key | Data1 | Data2 | Callback |
---|---|---|---|---|
ConditionAddPre | — | Barbarian (Cond Ref) | 0x0 | 0x100ecf30 |
ToHitBonusBase | — | 0x7 | 0x0 | 0x100fdf90 |
SaveThrowLevel | DK_SAVE_FORTITUDE | 0x7 | 0x0 | 0x100fe070 |
SaveThrowLevel | DK_SAVE_REFLEX | 0x7 | 0x0 | 0x100fe0c0 |
SaveThrowLevel | DK_SAVE_WILL | 0x7 | 0x0 | 0x100fe0c0 |
GetAC | — | 0x7 | 0x0 | 0x100feac0 |
GetAC | 0x8 | 0x7 | 0x0 | 0x100feb30 |
TakingDamage2 | — | 0x0 | 0x0 | 0x100feba0 |
You can see it has various Event entries. Each Event entry is associated with a callback hook (a function address within temple.dll that specifies the callback to use), which is in charge of handling that Event.
How is information relayed from the callback to the calculation of value? This is done through the Event Object.
An Event Object is created whenever the game dispatches an event. It holds and aggregates various kinds of data, depending on the context.
In the To Hit Bonus example, the Event Object happens to contain a Bonus List. This Bonus List, stored within the Event Object, is then modified by the various callback hooks which are attached to a character, such as from the above Barbarian Condition.
// The event object for getting Attack Bonuses and Armor Class
struct DispIoAttackBonus : DispIO {
int field_4;
AttackPacket attackPacket;
BonusList bonlist;
DispIoAttackBonus();
int Dispatch(objHndl obj, objHndl obj2, enum_disp_type dispType, D20DispatcherKey key);
};
Each Event Type has a particular Event Object associated with it. It may be initiated with various kinds and values of data, depending on context. See dispatcher.h for more details.
As you can see from the Condition Lists, Conditions encompass things like spell effects, class/race abilities and modifiers, feats, immunities, etc.
Each Condition definition (CondStruct) holds an array of callbacks associated with an Event, an event Key, and a couple of predefined parameters (Data1, Data2).
The Key field is basically a sub-type of the Event Type. A common example is the Query type for a D20Query event.
Data1/Data2 are generic pieces of data which may be used to customize a generic callback (e.g. Damage Type flags for a Damage Resistance callback), or contain a reference to a string or another Condition definition.
You've probably seen conditions applied to game characters in ToEE's various script files. For example, from the script file scr\Spell001 - Aid.py:
target_item.obj.condition_add_with_args( 'sp-Aid', spell.id, spell.duration, 0 )
This creates an instance of the 'sp-Aid' condition, and applies it to the target along with three Condition Arguments - values unique to that instance.
Each Condition defines the number of Arguments it has - these are internal variables used for state keeping.
In the above example for 'sp-Aid', as with most other Spell Effect conditions, the Spell ID is stored in the first argument, and the Remaining Duration is usually in the second argument. The third argument is usually a spell specific parameter, e.g. the for the Fire Shield spell it contains the Element type.
These values are accessible to the callback hooks. Thus they can be retrieved and/or manipulated. For example, you often use a D20Query event to retrieve data, and a D20SendSignal event to change data. In the python layer this might appear as
obj.d20_send_signal(S_SetPowerAttack, 5)
Arguments: 3
Dispatcher Hooks:
Event Type | Key | Data1 | Data2 | Callback |
---|---|---|---|---|
ConditionAddPre | — | Power Attack (Cond Ref) | 0x0 | 0x100ecf30 |
ConditionAdd | — | 0x0 | 0x0 | 0x100ed110 |
RadialMenuEntry | — | 0x0 | 0x0 | 0x100f8410 |
ToHitBonus2 | — | 0x0 | 0x0 | 0x100f84c0 |
DealingDamage | — | 0x0 | 0x0 | 0x100f8540 |
D20Signal | DK_SIG_SetPowerAttack | 0x0 | 0x0 | 0x100f8660 |
As you can see, the Power Attack has a hook for D20SendSignal, which is in charge of setting the power attack level (in the above case it will try to set it to 5).
Item-related conditions (such as "Damage Bonus") always keep the inventory index in their 3rd arg. This is maintained by the engine whenever it puts the item into an inventory slot. On that note, equipped items occupy slots 200-216.
Feat-related conditions will init the 1st arg with the feat enum, and the second arg (if applicable) may hold a feat "subtype". You may override these with an OnConditionAdd callback, as many feats do. If you don't, you may have unwanted initial values in those args, so beware!
Whenever a Condition is instantiated onto a character (a game object), it creates a Condition Node.
A Condition Node holds a reference to the general Condition Definition, and stores the argument values unique to that instance.
A Condition Node can generally be instantiated multiple times, though most of the Conditions prevent that with a standard callback that checks if the condition had already been applied. The relevant event is ConditionAddPre, which is triggered at the beginning of Condition application and may abort it.
A Dispatcher object is generated for each game object on the fly. They are not directly saved to HD, but are generated whenever a creature is spawned or a map is loaded.
This object simply holds references to all the Condition Nodes instantiated for the game object. It does separate them into 3 categories:
- Permanent Modifiers - things such as Class Conditions and Feats
- Item Modifiers - Conditions applied to the character owing to carried or wielded items
- Conditions - all the rest. The python commands
.condition_add
and.condition_add_with_args
go here.
Permanent Modifiers are always applied first when a character is spawned. The argument values are taken from the game object field obj_f_permanent_mod_data, which is where the game stores the data in between map transfers and in the savegame data.
Items conditions and their args are stored in obj_f_item_pad_wielder_condition_array and obj_f_item_pad_wielder_argument_array.
Normal Conditions are likewise stored in obj_f_conditions and obj_f_condition_arg0.
Note that the game reads the arguments sequentially, and they must be tightly synched to the number of arguments defined for each Condition. Thus, changing the number of Arguments a condition has is generally a VERY BAD IDEA! If you create a new Condition, pick a number - take a generous spare if you're not fully sure what you'll need, there's no shortage of RAM these days.
A Dispatcher Node (SubDispNode) is simply a linked list node containing a pointer to the Condition Node (CondNode*) and the hook definition (SubDispDef*). A dispatcher is basically an array of these linked lists - one linked list for each Dispatcher Type.
Note that Static objects are an exception - they have no Dispatchers.
TODO: translate my weird names to standard event system parlance. Until then, this diagram should explain everything.
Condition callbacks are looked up according to their Type in the proper context, and must match a Key to activate the callback (unless the key is null).
Example: Dispatcher Type 0x10 are called from the To Hit Roll functions. Therefore, conditions that need to modify the To Hit chance need to define a callback of that type.
The central function is the DispatcherProcessor, which is called with:
DispatcherProcessor( objHndl objHndCaller, Dispatcher * dispatcher, uint32_t dispType, uint32_t dispKey, void * dispIo )
The IO structure is generally
struct DispIO {
uint32_t IOType // usually gets checked in the Callbacks
uint32_t return_val
uint32_t data[]
}