Tutorial: Event Handlers

From GECK
Jump to: navigation, search

Required knowledge: UDFs, blocktypes, forms and refs; advanced use: arrays.
Also required: NVSE 4.6+


Why?

As a modder there are certain things that you know aren’t impossible, but you’d rather not do them because it causes conflicts and other problems.

How would you go about:

  1. altering a ‘personal hygiene’ variable every time someone activates a sink, tub or toilet?
  2. playing a woozy double vision image space modifier every time the player consumes booze?
  3. playing a wolf whistle sound every time an actor unequips their clothing?
  4. playing the theme from The Good, the Bad and the Ugly every time you equip a revolver?
  5. setting a chance of a booby trap materializing and going off if certain locked doors are opened by the player?
  6. monitoring changes in an actor’s AI packages, so that you can force them to stick to the one they’re supposed to follow?
  7. marking each item that is dropped from player inventory in a particular cell for deletion, to avoid savegame bloat, or to mimic things getting picked up because you’re not around to watch over them?
  8. having people say custom dialog whenever they hit other people with a particular weapon or weapon type?

The only way I can see so far of doing any of those is attaching an object script to an item, with a one-frame blocktype like OnActivate, On(Actor)Equip, OnOpen, OnPackageChange, OnDrop or OnHit(With) signaling that these events happen so you can do something about it.

The problem there is you can’t just go around attaching object scripts to vanilla forms without inviting conflicts with other mods changing anything about those forms, and creating a need for separate patch mods resolving those conflicts. FNV already has a significantly reduced maximum number of mods that can be loaded compared to other Beth games (this is no longer the case if using the FNV Mod Limit Fix NVSE plugin), and if a large portion of those are simple patches, that’s just a waste. Not to mention you have to make those patches, and keep repeating to people reporting problems to install them, in the right order.

Another issue is that you’ll usually need to attach the same script to a lot of objects if you want it to work correctly. This means all the base forms for sinks, toilets & tubs for your hygiene mod, or all the revolvers if you wanted to have people say "Yippee-ki-yay, motherfucker" if they hit others with revolvers. And what about mod-added items that could just as easily apply? Would you like to make a patch for each and every revolver mod out there? Not me.

A lot of times your script will also fire when it’s not needed: you want your item cleanup script to only work on some actors, or only want to have a chance of booby traps on some door refs while other doors sharing the same base form should be safe. The only way of detecting if the necessary conditions apply is by already running the script, and already inviting the conflicts by simply having it on the form.

These headaches are enough to make you want to skip adding some cool functionality altogether sometimes. What if...

  • we could be notified of anything being activated, opened, dropped, hit, consumed etc without attaching scripts to vanilla forms?
  • we could filter being notified according to what is being activated or who’s doing the activating, what is being opened or who’s doing the opening, etc?

That’d be sweet, huh? It’s exactly what event handlers do.

Standard events, handling them, parameters

Event handlers are UDFs that you register with NVSE to be called whenever their associated event happens. These handlers are then set for the rest of the game session; even if you load a different save, they are still valid until you actually restart the game. If I want to be notified whenever anything whatsoever is activated or dropped, for instance, I register an event handler with SetEventHandler every time the game is loaded:

SetEventHandler "OnActivate" myActivateEventHandler
SetEventHandler "OnDrop" myDropEventHandler

and then every time something is about to be activated or dropped, NVSE will call your event handler before proceeding with the activation or dropping. Remember that: certain functions that would work in a regular block will not in their equivalent event handler, because what's about to happen hasn't yet. Using the GetActionRef function in an "OnActivate" handler will probably not return anything - luckily you'll be provided with the same intel via the handler's parameters.

In the case of the “OnDropevent, NVSE will pass along the dropper as first parameter, and the dropped item as second. So your myDropEventHandler needs to be able to capture them, in that order:

Scn myDropEventHandler

ref rDropper
ref rDropped

Begin Function {rDropper, rDropped}
…
End

If you don’t have those variables ready to capture the parameters, nvse will report an error.

In the case of the “OnActivate” event, you need to remember that the terminology with the OnActivate script block and the activate function in vanilla can be confusing. Light switches, sinks and triggers are found under “activators” in the geck although they always seem to be activated by an actor. While it looks like an actor activates something like a container ref, when you force it via script it’s actually the container ref doing the activating:

rContainerRef.activate rActorRef

The “OnActivate” event, then, first expects the activator, the thing that looks to be activated, and then the thing or actor that seems to activate it, which you’d be able to retrieve from a regular OnActivate script block with GetActionRef. So let’s put it this way:

Scn myActivateEventHandler

Ref rActivator
ref rActionRef

Begin Function {rActivator, rActionRef}
…
End

Most of the standard event handlers, the ones that correspond with regular blocktypes, come with standard parameters like that. They will always be sent, be ready to receive them.

(The params don’t have to be called that, of course, the names are just meant to distinguish what they are.)

Using the standard parameters as filters

These handler parameters can be used as filters, by adding them as parameters to the SetEventHandler function when you register the handler. This makes it so that your event handler is only called in when it’s needed, and you should obviously use such filters as much as possible. Also make sure that whatever they filter on is actually a valid form, otherwise the filtering will bug out and it'll be as if no filter was applied.

Because you may choose to only use one filter, or none at all, you need to specifically state on which parameter you’re filtering, the first or the second, and couple them with double colons (like you do with ar_Map):

SetEventHandler "OnDrop" myEventHandler "first"::playerref "second"::myCoolgunForm

In OBSE, the “first” was called “ref”, and the “second” was called “object”, and this is still possible in NVSE:

SetEventHandler "OnDrop" myEventHandler "ref"::playerref "object"::myCoolgunForm

However, unlike OBSE, NVSE allows either of those filters to refer to a ref, base form, or even a formlist (!) containing refs and/or base forms and/or more formlists, when applicable. So using “first” and “second” is much less confusing in that regard.

So, give or take some scripting in the actual event handler scripts, the problems from the introduction should be a lot less problematic with the following events and filters set:

  1. SetEventHandler "OnActivate" myEventHandler "first"::formlistofallsinkforms "second"::playerref
  2. SetEventHandler "OnMagicEffectHit" myEventhandler "first"::playerref "second"::ChemIncCHAlcohol
  3. SetEventHandler "OnActorUnequip" myEventhandler ; check what’s unequipped with the handler’s parameter
  4. SetEventHandler "OnActorEquip" myEventhandler ; check what’s equipped with the handler’s parameter
  5. SetEventHandler "OnOpen" myEventHandler "first"::formlistofdoorrefs "second"::playerref
  6. SetEventHandler "OnPackageChange" myEventHandler "first"::rActor "second"::rPackage
  7. SetEventHandler "OnDrop" myEventHandler "first"::playerref ; check the cell and the dropped item in the handler
  8. SetEventHandler "OnHitWith" myEventHandler "second"::myCoolGunorListofGuns

For the package monitoring problem, it’s also entirely possible to monitor packages starting, changing and ending in one and the same handler:

SetEventHandler "OnPackageStart" myEventHandler "first"::rActor "second"::rPackage
SetEventHandler "OnPackageChange" myEventHandler "first"::rActor "second"::rPackage
SetEventHandler "OnPackageDone" myEventHandler "first"::rActor "second"::rPackage

In this case you can use the GetCurrentEventName function to figure out what event has called your handler:

Scn myEventHandler

Ref rActor
ref rPackage
string_var sv_event

Begin Function {rActor, rPackage}

Let sv_event := GetCurrentEventName
if eval sv_event == "OnPackageStart"
   ; do something
elseif eval sv_event == "OnPackageChange"
   ; do something else
endif

End

Removing event handlers

It’s entirely possible that you provide certain settings in your mod that cause an event to no longer apply. Also, event handlers are still valid if you re-load a different save but not re-start the game, so you want to build in some safety by removing a handler if, for instance, that different save hasn't gone through some init yet that the handling needs.

To remove a handler you use RemoveEventHandler, with the same syntax as the one you used when you set it:

RemoveEventHandler "OnActivate" myEventHandler "first"::rActivator "second"::rActionRef

The above will remove the event handler being called when those filters apply. If those are the only ones you set in the first place, you might as well skip the filters:

RemoveEventHandler "OnActivate" myEventHandler

So yes, you can set and remove an event handler being called under some filters and not others.

NVSE events

Aside from events related to vanilla blocktypes, you can also register handlers to be called when certain meta-game events happen. This is especially of use for mods that write some type of co-save files etc. (After testing, it seems this is for NVSE plugins only.)

User-defined events

As if all of that wasn’t cool enough, you can fire off custom events that other people can register to in the same way as they would a standard one, and receive information that you send along with it.

This is more naturally of use to modders working on some type of general functionality, wishing to let other modders know that something happened that is of note and non-continuous, something that can classify as an ‘event’.

Perhaps you’ve overhauled sneaking and wish to advertise that according to your sneaking mod, the player’s been detected or has successfully hid, whether or not vanilla functions agree.

Perhaps your mod needs to go through a series of init scripts before it’s functional and you wish to let dependent mods know that yours is ready for action when it’s done.

Perhaps you want your inventory sorter to not only notify others that sorting took place, but also send along information on where everything ended up.

In each of those cases, all you really need to do is dispatch an event with the DispatchEvent function, which should at least be followed by an event name, optionally a stringmap with intel, and optionally another string overriding the default sender name:

DispatchEvent "eventName":string args:stringmap sender:string

DispatchEvent "SomeEventName"
Array_var ar_mystringmap
; populate ar_mystringmap
DispatchEvent "SomeEventName", ar_mystringmap
DispatchEvent "SomeEventName", ar_mystringmap, "somestring"

And the mods wishing to register a handler to it will do it as normal, with the following limitations:

  • because there are no standard parameters to use, you can’t use filters when setting handlers for custom events
  • the only parameter that the handler UDF needs and has to have is an array var that can capture the stringmap that’s always received, even if none was dispatched:
SetEventHandler "SomeEventName" myEventHandler

Scn MyEventHandler

Array_var args

Begin Function {args}
…
End

If no stringmap was dispatched, it’ll only contain the 2 standard elements:

[eventName] == "SomeEventName"
[eventSender] == "SomeSenderString" (usually == "DispatchingMod.esp")

The dispatcher can determine “SomeSenderString” by explicitly adding the third optional parameter in the DispatchEvent line. If it’s left blank it’ll be the dispatching mod’s name. If a stringmap was dispatched, the standard elements are added to it.

That’s it. Go forward and modify.

External Links