Events and handlers

From ZDoom Wiki
Jump to navigation Jump to search
Note: This feature is for ZScript only.


Event handlers are a plugin-like system available since GZDoom 2.4 that allows your ZScript code to receive certain world, client and system events.
There can be multiple event handlers and in most cases all of them will receive the event (note: exceptions exist).

General information

An event handler is a class that inherits either StaticEventHandler or EventHandler. The differences between them are as follows:

Type Initialized Destroyed Serializable?
StaticEventHandler On GZDoom startup When closing GZDoom No
EventHandler At the beginning of every map At the end of every map Yes

For most of the level scripting you should use an EventHandler; StaticEventHandler is only needed if you need to store some non-playsim data locally.

Setting up

In order to make the event handler work, you need to declare it in MAPINFO. For this, two variants are supported: EventHandlers in map definition and EventHandlers or AddEventhandlers in gameinfo definition.

GameInfo
{
    AddEventHandlers = "MyEventHandler"
}

This will add an event handler MyEventHandler To the game. Multiple event handlers can be added with either a comma, or new AddEventHandlers definitions.

Note, this is a GameInfo section of the MAPINFO lump; not to be confused with the GAMEINFO lump, which is unrelated.

Functions

All event handler methods are defined as static and clearscope.

  • native Find(class<StaticEventHandler> type)
Retrieves a pointer to the specified event handler.
Use StaticEventHandler.Find to find static event handlers, and EventHandler.Find for non-static ones.

EventHandler only

  • static native void SendNetworkEvent(String name, int arg1 = 0, int arg2 = 0, int arg3 = 0)
Sends a network event, which can be intercepted in the NetworkProcess virtual override. Allows communication from UI scope to play scope. (See Networking.)
Only integer arguments are supported. To pass a string, pass it as part of the event's name and use Split to retrieve it. (See Networking for an example.)
  • static native void SendInterfaceEvent(int playerNum, string name, int arg1 = 0, int arg2 = 0, int arg3 = 0)
Sends an interface event, which can be intercepted in the InterfaceProcess virtual override. Allows communication from play scope to UI scope. (See Communicating with the UI.)

Handling events

Each possible event is done as a virtual method in the StaticEventHandler/EventHandler classes which can be overridden.
Note that you don't need to call the original methods because the default implementation is empty and does nothing.

Method Data available (in the provided event) Description
void OnEngineInitialize () Called right after the engine starts up (After messages like CPU information print). This can only be used on StaticEventHandlers. (New from 4.11.0)
void OnRegister () Called when the engine registers your handler (adds it to the list).
Event handler order setup can be performed here.
void OnUnregister () Called when the engine removes your handler from the list.
void WorldLoaded (WorldEvent e)
  • bool IsSaveGame
  • bool IsReopen
IsSaveGame only works for StaticEventHandlers. By using this field you can detect that the current level was loaded from a saved game.
IsReopen will be true when the player returns to the same map in a hub, similar to REOPEN ACS script type.
void WorldUnloaded (WorldEvent e)
  • bool IsSaveGame
  • String NextMap
IsSaveGame only works for StaticEventHandlers. By using this field you can detect that the current level is unloaded to load a saved game.
NextMap is the name of the map to be entered next, if any.

void WorldThingSpawned (WorldEvent e)
void WorldThingDied (WorldEvent e)
void WorldThingRevived (WorldEvent e)
void WorldThingDestroyed (WorldEvent e)

  • Actor Thing

WorldThingDied only:

  • Actor Inflictor
These events are received just after the specified Thing was spawned or revived/raised/resurrected (including the player's use of the resurrect cheat), and just before it dies or gets destroyed.

Internally, WorldThingSpawned is called just after PostBeginPlay.

If Inflictor is not null, it is the actor that caused the damage that killed Thing. While the event does not provide a pointer to the source of the damage (killer), it, if not null, is stored in the target field of Thing.

void WorldThingDamaged (WorldEvent e)
  • Actor Thing
  • Actor Inflictor
  • int Damage
  • Actor DamageSource
  • Name DamageType
  • EDmgFlags DamageFlags
  • double DamageAngle
The arguments for this event are the same as the DamageMobj arguments.

DamageAngle can be different from direct angle to Inflictor if portals are involved — beware.
Thing is the actor that got damaged.

void WorldThingGround (WorldEvent e)
  • Actor Thing
  • State CrushedState
Called when Thing actor's corpse is crushed into gibs. CrushedState is the crush state the actor had entered.

void WorldLineDamaged (WorldEvent e)
void WorldSectorDamaged (WorldEvent e)

  • Actor DamageSource
  • int Damage
  • int NewDamage
  • Name DamageType
  • vector3 DamagePosition
  • bool DamageIsRadius

WorldLineDamaged only:

  • Line DamageLine
  • int DamageLineSide

WorldSectorDamaged only:

  • Sector DamageSector
  • SectorPart DamageSectorPart
These events are called before dealing damage to a line or sector, which allows the events to manipulate the damage through Damage (raw damage) and NewDamage (damage after modification).
void WorldLightning (WorldEvent e) Same as LIGHTNING ACS script type.
void WorldTick () Calls at the beginning of each tick, 35 times per second.
void WorldLinePreActivated (WorldEvent e)
  • Actor Thing
  • Line ActivatedLine
  • int ActivationType
  • bool ShouldActivate
This event is called upon the activation of a line, right before the line's special is executed.
  • Thing is the activating actor.
  • ActivatedLine is the activated line. This field is read-only, and thus cannot be changed.
  • ActivationType is the line's method of activation. This is not how it was activated by the actor. This field is read-only, and thus cannot be changed.
  • ShouldActivate determines whether or not to continue with the activation process. If this is set to false, activation is aborted early on, before even the execution of the line's special and the triggering of the WorldLineActivated event, for example.
void WorldLineActivated (WorldEvent e)
  • Actor Thing
  • Line ActivatedLine
  • int ActivationType
This event is called upon the activation of a line, after the line's special has been successfully executed.
  • Thing is the activating actor.
  • ActivatedLine is the activated line. This field is read-only, and thus cannot be changed.
  • ActivationType is the line's method of activation. This is not how it was activated by the actor. This field is read-only, and thus cannot be changed.

void PlayerEntered (PlayerEvent e)
void PlayerSpawned (PlayerEvent e)
void PlayerRespawned (PlayerEvent e)
void PlayerDied (PlayerEvent e)
void PlayerDisconnected (PlayerEvent e)

  • int PlayerNumber
  • bool IsReturn
These are generally the same as their ACS counterparts.
PlayerEntered is called when players connect.
PlayerSpawned is called when the player spawns in the level, much like an ENTER script.
PlayerRespawned calls when players respawn (or use the resurrect cheat).
PlayerDied calls when players die (along with WorldThingDied)
PlayerDisconnected calls at the same point as DISCONNECT scripts (this is generally useless with P2P networking).

PlayerNumber is the player that was affected. You can receive the actual player object from the global players array like this:

 PlayerInfo player = players[e.PlayerNumber];

IsReturn will be true if this player returns to this level in a hub.

void RenderOverlay (RenderEvent e)
void RenderUnderlay (RenderEvent e)

  • Vector3 ViewPos
  • double ViewAngle
  • double ViewPitch
  • double ViewRoll
  • double FracTic
  • Actor Camera
These events can be used to display something on the screen. Elements drawn by these events are drawn underneath the console and menus. The difference between the two events, is that elements drawn by RenderOverlay are drawn over the HUD, while elements drawn by RenderUnderlay are drawn underneath it.

Note that it works locally and in ui context, which means you can't modify actors and have to make sure what player you are drawing it for (using consoleplayer global variable).

bool UiProcess (UiEvent e)
  • EGUIEvent Type
  • String KeyString (only for key events)
  • int KeyChar (only for key events)
  • int MouseX (only for mouse events)
  • int MouseY (only for mouse events)
  • bool IsShift
  • bool IsAlt
  • bool IsCtrl
By using this event you can receive UI input in the event handler. UI input is different from game input in that you can receive absolute screen mouse position instead of mouse deltas, and keyboard events are a bit more suitable for text input.

This event will only be received if you set your EventHandler in UI mode, e.g. by doing this:

 self.IsUiProcessor = true;

Additionally, mouse events will only be received if you also set RequireMouse to true:

 self.RequireMouse = true;

KeyChar is the ASCII value for the key, while KeyString is a single-character string that contains the character provided for convenience, as ZScript doesn't provide a char type.

Note: this is one of the few non-void methods in the event system. By returning true here you will block any processing of this event by the other event handlers if their Order is lower than the current EventHandler.
Also, if you need to interact with the world upon receiving an event, you have to use EventHandler.SendNetworkEvent (see: networking).

bool InputProcess (InputEvent e)
  • EGenericEvent Type
  • int KeyScan
  • int KeyChar
  • String KeyString
  • int MouseX (only for mouse event)
  • int MouseY (only for mouse event)
This event provides direct interface to the commonly used player input. You don't need any special steps in order to use it.

MouseX and MouseY are delta values (offsets from the last mouse position). These are internally used for player aiming.

KeyScan is the internal ASCII value of the pressed key while KeyChar is the real ASCII value of the pressed key. KeyString is a single-character string that contains KeyScan provided for convenience. Converting KeyChar to a string is not guaranteed to be the same as KeyString. Note that, while a bit counter-intuitive (for people unfamiliar with the console bind system), mouse buttons are considered keys for this event.
For example, a left mouse click is registered as KeyDown+InputEvent.Key_Mouse1.

Note: this is one of the few non-void methods in the event system. By returning true here you will block any processing of this event by the other event handlers if their Order is lower than the current EventHandler.
In case of InputEvent, returning true will also mean that the generic game input handler will NOT receive the event (player will be locked from moving).
Also, if you need to interact with the world upon receiving an event, you have to use EventHandler.SendNetworkEvent (see: networking).

void UiTick () This is the same as WorldTick, except it also runs outside of the level (only matters for StaticEventHandlers) and runs in the ui context.
void ConsoleProcess (ConsoleEvent e)
  • String Name
  • int Args[3]
This event is called when the player uses the "event" console command. It runs in the ui context.

For example, when the player runs this command:

 event testevent 1 2 3

The event handler will receive Name as "testevent" and Args[0]...Args[2] as {1,2,3}.

void NetworkProcess (ConsoleEvent e)
  • int Player
  • String Name
  • int Args[3]
  • bool IsManual
This event is called either when the player uses the "netevent" console command, or when EventHandler.SendNetworkEvent is used.

To distinguish between these two cases, you can use IsManual. This field will be true if the event was produced manually through the console.
Player is the number of the player that activated the event.
This is generally similar to using the "puke" command for ACS.

void InterfaceProcess (ConsoleEvent e)
  • String Name
  • int Args[3]
  • bool IsManual
This event is called either when the player uses the "interfaceevent" console command, or when EventHandler.SendInterfaceEvent is used.

To distinguish between these two cases, you can use IsManual. This field will be true if the event was produced manually through the console.
Similar to ConsoleProcess, this is ran in the ui scope and can be used to set information on menus and the HUD from the play scope.

void CheckReplacement (ReplaceEvent e)
  • Class<Actor> Replacee
  • Class<Actor> Replacement
  • bool IsFinal
This event is called when performing actor class replacements.
  • Replacee is the actor class being replaced. It can be used to check for specific actor classes to replace. This field is read-only and cannot be changed.
  • Replacement is the actor class the Replacee is to be replaced with.
  • IsFinal allows different CheckReplacement() overrides to interact: the value serves as a 'marker' of sorts, to tell the other event handlers with a CheckReplacement() override that this replacement should be considered final. Note, this does not allow to actually enforce your replacement; the other event handlers have to manually respect it by checking that e.isFinal is false before setting their replacement.

Replacing actor classes through this method has precedence over both the skill method and the replaces-keyword method in DECORATE and ZScript (regardless of the value of isFinal).

void CheckReplacee (ReplacedEvent e)
  • Class<Actor> Replacee
  • Class<Actor> Replacement
  • bool IsFinal
This is called by functions such as A_BossDeath or any other replacee checkers. When using CheckReplacement instead of the 'replaces' keyword directly for actors, those functions check if there is a replacement for monsters such as the Arachnotron with the Doom 2 MAP07 specials. By indicating the replacee is an Arachnotron for example, this will ensure that all the monsters who call those functions will not trigger the special until all replacees of Arachnotron are dead.
  • Replacee is the actor class being replaced. It can be used to check for specific actor classes to replace.
  • Replacement is the actor class the Replacee is to be replaced with. This field is read-only and cannot be changed.
  • IsFinal allows different CheckReplacee() overrides to interact: the value serves as a 'marker' of sorts, to tell the other event handlers with a CheckReplacee() override that this replacement should be considered final. Note, this does not allow to actually enforce your setting; the other event handlers have to manually respect it by checking that e.isFinal is false.
void NewGame () This event is called upon starting a new game. It is also called upon entering a titlemap, as well as upon being reborn after death without a saved game.

Event handler order

Event handler order defines the order in which user interaction-related events are received by the handlers.
It also defines the order in which RenderOverlay events are received.

For input events, the higher order receives the event first, while for render events the higher order receives the event last (thus drawing on top of everything).

Events that are reverse ordered:

  • PlayerDisconnected
  • RenderOverlay
  • WorldThingDestroyed
  • WorldUnloaded

You can set the order only in OnRegister callback, like this:

override void OnRegister()
{
	SetOrder(666);
}

The value is arbitrary and it only matters in relation to other event handler order. Default order is 0.

Communicating with the UI

Sometimes sending information from the playsim to the UI is needed, but this can be difficult to set up since the playsim has no access to the UI whatsoever. EventHandler.SendInterfaceEvent cam be used to pass select information over similar to a network event. It's ran instantly across all machines so the player number is needed to know which one should execute it. If you want an interface event to trigger on everyone's machine, passing consoleplayer as the first argument will do this. Passing net_arbitrator can be used to always send information over to the host of a game when playing online for potential admin handling.

class SpecialHUDEvents : EventHandler
{
	override void WorldThingDied(WorldEvent e)
	{
		// Pass information to the status bar that a player killed a monster
		if (e.thing && e.thing.bIsMonster && e.thing.target && e.thing.target.player)
		{
			EventHandler.SendInterfaceEvent(e.thing.target.PlayerNumber(), "KilledMonster");
		}
	}

	override void InterfaceProcess(ConsoleEvent e)
	{
		if (!e.isManual && e.name ~== "KilledMonster")
		{
			let sb = CustomStatusBar(statusBar);
			if (sb)
			{
				sb.KilledMonster();
			}
		}
	}
}

Networking

Since GZDoom 2.4, scripts that are allowed to directly interact with the user and scripts that are actively involved in the world processing are separated. World scripts are called "play scope" and user interaction scripts are called "ui scope". Altering the world from ui context is not possible; for details see object scopes and versions.

In order to perform some world action when the user clicks in a menu or presses some key in an InputEvent handler, you need to use the combination of EventHandler.SendNetworkEvent and an event handler that has an override of NetworkProcess.

In your ui, you execute it as follows:

 EventHandler.SendNetworkEvent("myevent", 666, 23, -1);

Then any event handlers that handle NetworkProcess will receive an event with Name equal to "myevent" and Args equal to {666,23,-1}.

Passing strings to network events

Any event handler can handle any name, so if you need to send a string you can simply check for Name starting with a value, for example (space does not work for this when typing directly into the console - you need an alias or SendNetworkEvent, or put the thing in quotes):

 EventHandler.SendNetworkEvent("giveitem:BFG9000");

where the event handler in question will find the strings on either side of the colon and deal with them accordingly:

class ItemGiveEventHandler : EventHandler
{
	override void NetworkProcess (ConsoleEvent e)
	{
		if (e.IsManual || e.Player < 0 || !PlayerInGame[e.Player] || !(players[e.Player].mo))
			return;

		let player = players [e.Player].mo;
		
		Array<string> command;
		e.Name.Split (command, ":");

		if(command.Size() == 2 && (command [0] ~== "giveitem"))
		{
			player.GiveInventory (command [1], 1);
		}
	}
}

Without the IsManual check, this event handler as written would let any player type "netevent giveitem:BFG9000" ingame to get a BFG and the engine would not recognize it as a cheat.

Network Commands and Buffers

As of GZDoom 4.12, a new API for sending custom network events has been added. This is sent via NetworkBuffers and received thru NetworkCommands. This API allows for much more extensive handling of network messages by allowing the ability to send any basic data type over the network (ints, doubles, and strings) and with a variable amount of them as opposed to the string and three ints of network events. These also can't be sent through the console making them exclusive to its usage within ZScript, offering more basic safety out of the box.

EventHandler.SendNetworkCommand is the simplest way to send a message. This takes a Name for the command followed by an unlimited amount of data pairs. The first argument of the pair describes what type the data is e.g. NET_INT, NET_DOUBLE, etc., and the next value is the actual data being sent.

 EventHandler.SendNetworkCommand('MyCmd', NET_DOUBLE, 5.2342, NET_INT, 7, NET_STRING, "Hello world!");

This can even be sent with nothing but a name if you have no data to send, making it much more efficient than using network events.

Processing of these commands is done within a handler's NetworkCommandProcess function. This takes a NetworkCommand that contains the client id who sent it, the command name, and the network message itself. This is safe in that it contains only the message sent by this command and won't error out if reaching the end of the internal buffer. A set of Read* functions allow the buffer to be parsed. If we follow the network command from the above example, we would read it as:

class MyEventHandler : EventHandler
{
	override void NetworkCommandProcess(NetworkCommand cmd)
	{
		if (cmd.Command == 'MyCmd')
		{
			double myDouble = cmd.ReadDouble();
			int myInt = cmd.ReadInt();
			string myString = cmd.ReadString();
			
			// Prints "5.2342, 7, Hello world!"
			Console.Printf("%.4f, %d, %s", myDouble, myInt, myString);
		}
	}
}

A more advanced method of sending data is a NetworkBuffer. This allows data to be built up about a network message if the specifics of it aren't entirely known before sending it. This has similar functions to NetworkCommands except they use Add* instead to specify what type of data should be written to the buffer e.g. AddDouble, AddString, etc. Note: this doesn't write directly to the actual network buffer but is merely a wrapper for sending a full command to it. EventHandler.SendNetworkBuffer is similar to SendNetworkCommand except instead of a variable list of arguments it takes a NetworkBuffer object.

NetworkBuffer myBuffer = new("NetworkBuffer");
myBuffer.AddDouble(5.2342);
myBuffer.AddInt(7);
myBuffer.AddString("Hello world!");

EventHandler.SendNetworkBuffer('MyCmd', myBuffer);

Data can be sent in smaller chunks e.g. AddInt8/NET_INT8 can be used to send a number over as an 8-bit byte instead of a full 32-bit value. This can be useful for saving on network traffic but should only be used if you absolutely know the value range of something being sent (e.g. anything outside that range is considered invalid). Otherwise if you don't know the full extent of the value it's best to use full-sized data types to prevent any data loss. This can also be used for custom quantizing of values if needed.

Serializing structs/objects

Given the above functionality, it's possible to use this to create custom serializing functions for large data types/objects. In particular this is useful with objects because without a custom network id set up, there's no way to handle sending an object over the network without sending over its data and reconstructing it on the other side (these things are tracked as memory references which naturally are going to be different on every client's machine). Serialization functions are a form of unified function that handles both writing and reading data to keep it isolated to one location, preventing possible mistakes. There are multiple ways to set this up, but below is a very basic implementation on objects:

class MyObject play
{
	int iData;
	bool bData;
	Array<double> fData;

	void Serialize(NetworkCommand cmd = null, NetworkBuffer buffer = null)
	{
		if (cmd) // Reading.
		{
			iData = cmd.ReadInt();
			bData = cmd.ReadInt8();
			cmd.ReadDoubleArray(fData);
		}
		else if (buffer) // Writing.
		{
			buffer.AddInt(iData);
			buffer.AddInt8(bData);
			buffer.AddDoubleArray(fData);
		}
	}
}

	// Sending the object.
	NetworkBuffer buffer = new("NetworkBuffer");
	myObj.Serialize(buffer: buffer);
	EventHandler.SendNetworkBuffer('MyObj', buffer);

	// Reading the object back.
	if (cmd.Command == 'MyObj')
	{
		MyObject myObj = new("MyObject");
		myObj.Serialize(cmd);
	}

Destruction

Event handlers inherit from Object, and can be destroyed by calling the Destroy() function. This is useful for one-off events, since the list of functions being called (see above) is large and can potentially slow down the game.

There are a few circumstances to be aware of:

  • Make sure the handler is absolutely unneeded before destroying. Unlike other objects, event handlers require special initialization by the engine and cannot be recreated with 'new' keyword.
  • Regular event handlers are recreated after each map is loaded and are automatically destroyed when leaving a hub cluster or non-hub map, but static handlers are only created when the game starts up.
  • Avoid calling Destroy() in subfunctions. Processing code can still execute and cause unpredictable behavior or crash the game. An ideal way to destroy a handler is as follows:
private bool DestroyMe;
override void WorldTick()
{
	if (DestroyMe) // If we no longer need this event handler, get rid of it.
	{
		if (!bDESTROYED) // Safety check, a flag found in Object itself.
			Destroy();
		return;
	}
}

Examples

Getting an event handler. This ensures that event handlers are properly found, and are the correct type. Put this inside the event handler, and replace 'MyEventHandler' with the name of the handler that's given this function.

// If put outside an event handler, you would add 'EventHandler.' behind Find.
static clearscope MyEventHandler Fetch()
{
	return MyEventHandler(Find("MyEventHandler"));
}


Using CheckReplacement, this example replaces all monsters with archviles.

override void CheckReplacement (ReplaceEvent e)
{
	if (GetDefaultByType(e.Replacee).bIsMonster)
	{
		e.Replacement = 'Archvile';
	}
}

This event handler prints the class name of any Actor that spawns

version "2.4" // Make sure to use version 2.4 or later

class NamePrinter : EventHandler
{
	override void WorldThingSpawned(WorldEvent e)
	{
		if (e.thing) // Check that the Actor is valid
			console.printf("%s", e.thing.GetClassName());
	}
}

In the MAPINFO:

GameInfo
{
    AddEventHandlers = "NamePrinter"
}

See also the Hello World page, which implements a basic "Hello World!" script using eventhandlers.