Modding:Events

From Caves of Qud Wiki
Revision as of 13:22, 2 July 2022 by Egocarib (talk | contribs)
Jump to navigation Jump to search
This page is about modding. See the modding overview for an abstract on modding.
This page is about modding. See the modding overview for an abstract on modding.
For best results, it's recommended to have read the following topics before this one:

Events are the basic building block that most of Caves of Qud's mechanics are built upon.
When any object spawns, attacks, moves, dies and etcetera: an event is fired. This gives any number of registered listeners to that event a chance to react and execute their own code.
You can read more about traditional application events in Microsoft's C# Programming Guide, the implementation is different but they fulfill the same function.

Standard Events

Events are typically listened for in generic Parts or Effects, which is what implements more advanced functionality of an object.
Mutations (Creation) and Skills are both Parts. Effects such as sleep, prone or poison are handled separately from Parts as they are usually temporary.

Listening

To listen for an event you typically override the Register method of the base IPart or Effect class.
This is executed once when the IPart/Effect is added to the object and subsequently serialized.
Overriding the AllowStaticRegistration method to return true alters this behaviour: registrations are no longer serialized and Register is re-executed every save load.
It is however possible to register for an event anywhere as long as you have an instance of both an IPart/Effect and a GameObject.

public override void Register(GameObject obj) {
	// Listen for when the GameObject in obj is awarded XP.
	obj.RegisterPartEvent(this, "AwardXP");

	// The otherwise identical method call used for when [this] is an Effect.
	obj.RegisterEffectEvent(this, "AwardXP");

	// Call the base Register method that we overrode.
	base.Register(obj);
}

Firing

To fire an event you call the FireEvent method on the relevant GameObject and pass it an Event with optional parameters.
You can later extract these parameters when handling the event.

public void AwardPlayerXP() {
	// Get the player GameObject.
	GameObject player = XRLCore.Core.Game.Player.Body;

	// Create a new "AwardXP" Event, specifying an "Amount" parameter with the integer value 50.
	// You can keep declaring staggered parameters or leave them out entirely.
	// Event.New(ID, [PrmName1, PrmVal1, PrmName2, PrmVal2, PrmName3, ...])
	Event awardXP = Event.New("AwardXP", "Amount", 50);

	// You can also set the parameters one by one instead of passing them to the constructor.
	awardXP.SetParameter("Amount", 50);

	// Fire the event on the player.
	player.FireEvent(awardXP);
}

Handling

Once the event has been fired it will call the FireEvent method on all registered Parts and Effects, where you can handle it and execute your own code.

public override bool FireEvent(Event E)
{
	// If the ID of our event is "AwardXP"...
	if (E.ID == "AwardXP") {
		// Get the parameter we specified either in the constructor or using SetParameter.
		int amount = E.GetIntParameter("Amount");

		// Add the amount to this GameObject's experience.
		this.ParentObject.Statistics["XP"].BaseValue += amount;
	}

	// Return the result of the base FireEvent that we overrode, which returns a literal true.
	// Should you instead return false further event processing will stop, meaning no Parts and Effects after get the chance to handle the event.
	return base.FireEvent(E);
}

Minimal Events

Minimal events are a new event system introduced with the Tomb of the Eaters update (V200).
While the underlying implementation has changed drastically, the usage is familiar in that you still listen, fire and handle events.

Listening

To listen for a min event you override the WantEvent method of the base IPart or Effect class.
This will be executed every time an event is fired so keep it lean.
Unlike regular events, WantEvent is the only way to report you'd like to handle a min event. If you need to dynamically listen for another event it's recommended to do your dynamic logic elsewhere and flip a boolean variable for the WantEvent.

// Listens for one event always.
public override bool WantEvent(int ID, int cascade) {
	// Check if the ID parameter matches one of the events we want, in this case ZoneActivatedEvent.
	// The base WantEvent of IPart/Effect will always return false.
	return base.WantEvent(ID, cascade) || ID == ZoneActivatedEvent.ID;
}


// Flip this boolean based on some logic condition elsewhere.
bool wantEndTurn = false;

// Listens for an event dynamically.
public override bool WantEvent(int ID, int cascade) {
	if(ID == ZoneActivatedEvent.ID)
		return true;
		
	if(wantEndTurn && ID == EndTurnEvent.ID)
		return true;

	return base.WantEvent(ID, cascade);
}

Firing

To fire a min event you call the HandleEvent method on the relevant GameObject and pass it a specific MinEvent.
Setting parameters on a MinEvent is a lot more intuitive compared to the old system as they are now properties of their own type.

public void GivePlayerDrams() {
	// Get the player GameObject.
	GameObject player = XRLCore.Core.Game.Player.Body;

	// Retrieve a GiveDramsEvent from the pool, this is the preferred way to get an event instance.
	GiveDramsEvent E = GiveDramsEvent.FromPool();
	// Some events have overloaded FromPool to set properties of the event on retrieval.
	E = GiveDramsEvent.FromPool(Actor: player, Drams: 50);
	// You can still set properties directly.
	E.Liquid = "water";
	// Yet other events do not have an accessible FromPool method, in which case you'll have to instantiate your own.
	E = new GiveDramsEvent() {
		Actor = player,
		Drams = 50
	};

	// Fire the event on the player.
	player.HandleEvent(E);
}

Handling

The handling of min events is the most drastic change from the old system, as you can now override one separate method for each type of MinEvent which is much simpler to organize.

// A volume/container of liquid, like a puddle or flagon.
LiquidVolume liquid = new LiquidVolume();

// This method handles the GiveDramsEvent.
public override bool HandleEvent(GiveDramsEvent E) {
	liquid.GiveDrams(E.Liquid, ref E.Drams, E.Auto);

	// Just like old events, returning false prevents further event processing.
	// Here we have no more liquid left to give so there's no reason to continue.
	if (E.Drams <= 0)
		return false;

	return true;
}

// This method handles the FrozeEvent.
public override bool HandleEvent(FrozeEvent E) {
	E.Object.DisplayName = "&CFrozen Object";
	return true;
}

// It's still possible to override the base HandleEvent and compare the ID or type like old events.
// This is more useful for when you're cascading events carte blanche to sub-objects, see the Cascading section below.
public override bool HandleEvent(MinEvent E) {
	if (!base.HandleEvent(E)) {
		return false;
	}
	
	if (E.ID == GiveDramsEvent.ID) {
		return HandleEvent(E as GiveDramsEvent);
	} else if (E is FrozeEvent) {
		return HandleEvent(E as FrozeEvent);
	}

	return true;
}

Custom

Prior to patch 200.43 it was possible to simply overload the handling methods, where it was then retrieved & invoked through reflection based on the parameter type and cached in a dictionary.
If you are creating your own custom MinEvent, there's nothing for you to override so you will still have to do this. To mark your custom MinEvent for invocation, override the WantInvokeDispatch method to return true.

public class MyCustomMinEvent : MinEvent
{
	public new static readonly int ID = MinEvent.AllocateID();

	public MyCustomMinEvent() {
		base.ID = MyCustomMinEvent.ID;
	}

	// This is necessary for custom events that need the HandleEvent to be reflected.
	public override bool WantInvokeDispatch() {
		return true;
	}

	// A static helper method to fire our event on a GameObject.
	// If it's a high-frequency event this should implement event pooling, rather than create a new MyCustomMinEvent each time.
	public static void Send(GameObject Object) {
		if (Object.WantEvent(MyCustomMinEvent.ID, MinEvent.CascadeLevel)) {
			Object.HandleEvent(new MyCustomMinEvent());
		} 
		// If you want to use an old event as a fallback, this is a good place to do so.
		if (obj.HasRegisteredEvent("MyCustomEvent")) {
			obj.FireEvent(Event.New("MyCustomEvent"));
		}
	}
}

// The reflected overload method that will handle our event in a part or effect.
public bool HandleEvent(MyCustomMinEvent E) {
	return true;
}

Cascading

In some cases events should be handled by objects contained within your Part, such as items in the Inventory or equipped on your Body.
This is what the CascadeLevel of the event dictates. The cascade level is a bit field which determines when and where the event should cascade depending on which bits are flipped.

// The cascade category bits of MinEvent.
public const int CASCADE_NONE = 0x0;                  // 0b00000
public const int CASCADE_EQUIPMENT = 0x1;             // 0b00001
public const int CASCADE_INVENTORY = 0x2;             // 0b00010
public const int CASCADE_SLOTS = 0x4;                 // 0b00100
public const int CASCADE_COMPONENTS = 0x8;            // 0b01000
public const int CASCADE_EXCEPT_THROWN_WEAPON = 0x10; // 0b10000
public const int CASCADE_ALL = CASCADE_EQUIPMENT | CASCADE_INVENTORY | CASCADE_SLOTS | CASCADE_COMPONENTS;

// A list of items.
List<GameObject> Inventory = new List<GameObject>();

public override bool WantEvent(int ID, int cascade)
{
	// If the cascade variable has the inventory bit...
	if (MinEvent.CascadeTo(cascade, MinEvent.CASCADE_INVENTORY)) {
		// Check if any item in our inventory wants this event.
		foreach(GameObject item in Inventory) {
			if(item.WantEvent(ID, cascade))
				return true;
		}
	}
	
	return base.WantEvent(ID, cascade);
}

public override bool HandleEvent(MinEvent E)
{
	// If the CascadeLevel has the inventory bit...
	if (E.CascadeTo(MinEvent.CASCADE_INVENTORY)) {
		// Allow each item in the inventory to handle the event.
		// Stops processing should any of them return false.
		foreach(GameObject item in Inventory) {
			if(!item.HandleEvent(E))
				return false;
		}
	}
	
	return base.HandleEvent(E);
}