diff --git a/docs/events.md b/docs/events.md new file mode 100644 index 0000000..27eac33 --- /dev/null +++ b/docs/events.md @@ -0,0 +1,392 @@ +# Signals and slots system + +Acedia provides its own unique event system that is more powerful than regular +`delegate`s and easier to use than listener-type objects like `GameRules`. +It's best demonstrated with an example from AcediaFixes' feature +that deals with friendly fire-related exploits. +Said feature has to catch and handle `NetDamage()` event from `GameRules` and, +with Acedia's signal/slot system, handler can be added with a single line: + +```unrealscript +_.unreal.gameRules.OnNetDamage(self).connect = NetDamageHandler; +``` + +`OnNetDamage()` is a *signal function* responsible for adding new handlers +for the `NetDamage` event. +Function `NetDamageHandler` has to have an appropriate signature +(parameters and return value types) for the event and will be called each time +`NetDamage` event occurs, starting from the next occurrence. + +Unlike raw `delegate`s, that can each only store one function, signal/slot +system allows us to have however many handlers we need: + +```unrealscript +// All of those will be called on `NetDamage` event +_.unreal.gameRules.OnNetDamage(self).connect = NetDamageNext; +_.unreal.gameRules.OnNetDamage(self).connect = NetDamageTry; +_.unreal.gameRules.OnNetDamage(self).connect = NetDamageRevolution; +_.unreal.gameRules.OnNetDamage(self).connect = NetDamageEvolutionR; +``` + +`self` argument is necessary for the cleanup and refers to the object that is +responsible for the added handler - if it gets deallocated, then all of +the associated handlers will be automatically removed: + +```unrealscript +// This assumes that `someObj` is a child class of `AcediaObject` +_.unreal.gameRules.OnNetDamage(someObj).connect = NetDamageHandler; +_.memory.Free(someObj); // After this line `NetDamageHandler()` won't be used +``` + +Most of the time this parameter is going to be `self`, since normally each +object adds its own functions as event handlers. + +Once you no longer need to handle an event, you can *disconnect* from it: + +```unrealscript +_.unreal.gameRules.OnNetDamage(self).Disconnect(); +// To disconnect some object `someObj`: +_.unreal.gameRules.OnNetDamage(someObj).Disconnect(); +``` + +This will remove all handlers associated with the object passed as +an argument. + +> **NOTE:** +> Even though handlers associated with deallocated object will be automatically +> removed, it is still good practice to manually remove all handlers associated +> with it inside a finalizer, since otherwise "dead" handlers will be stored +> until related event is triggered. + +Removing individual handlers is also possible, but can be a bit cumbersome and +is not recommended. + +> **NOTE:** +> Our signals and slots take this moniker after [Qt](https://doc.qt.io)'s +> events system; +> however, what they are and how they work is different and shouldn't +> be mixed up. + +## [Advanced] How signals and slots work + +### Delegates overview + +This system was created because of limitations of `delegate`s in UnrealEngine 2. +One can declare `delegate` inside any class: + +```unrealscript +class MyClass extends Object; + +delegate MyDelegate() +{ + // This code will be executed if no function is assigned to the delegate. + Log("Empty message"); +} +``` + +then assign a function to them and any time a delegate is called - an assigned +function will be called instead: + +```unrealscript +function handler() +{ + Log("Handler is called!"); +} + +local MyClass obj; +obj = new class'MyClass'; +obj.MyDelegate(); // "Empty message" is logged +obj.MyDelegate = handler; +obj.MyDelegate(); // "Handler is called!" is logged +obj.MyDelegate = none; // Reset delegate to its default state +obj.MyDelegate(); // "Empty message" is logged +``` + +However they have their limitations, main one being that you can neither assign +several functions to a `delegate` nor can you create an array of `delegate`s to +have several handlers for your events. + +### `Slot`s are boxed `delegate`s, `Signal`s are arrays of `Slot`s + +Acedia bypasses this limitation by essentially boxing `delegate`s. +If you are unfamiliar with the concept of boxing, it is discussed +[here](./objects.md). +`Slot` is just an object that contains a single `delegate` (usually called +`connect()`) with some extra code to support cleanup of no longer needed +`Slot`s. +Wrapping `delegate`s into `Slot`s allows us to store them in arrays represented +by `Signal`s: each `Signal` is usually associated with some sort of an event and +can refer (be connected to) several `Slot`s, therefore supporting several +different handlers for its event. + +`Signal`s are usually declared as internal variables and are different from +*signal function* like `_.unreal.gameRules.OnNetDamage()`. +Connecting a handler to an event with line like +`OnNetDamage(self).connect = NetDamage` +actually results in performing following steps: + +1. Appropriate `Signal` object is found / accessed; +2. New `Slot` object for that `Singal` is created and returned; +3. `connect` delegate for returned `Slot` gets assigned with a handler function + (`NetDamage` in above example). + +```unrealscript +// [GameRulesAPI.uc] +public final function GameRules_OnNetDamage_Slot OnNetDamage( + AcediaObject receiver) +{ + local Signal signal; + local UnrealService service; + // These two lines are implementation detail for `OnNetDamage()`, + // you can store your `signal` object wherever you need. + service = UnrealService(class'UnrealService'.static.Require()); + signal = service.GetSignal(class'GameRules_OnNetDamage_Signal'); + // This is the important line that creates new slot + return GameRules_OnNetDamage_Slot(signal.NewSlot(receiver)); +} + +// [FixFFHack.uc] +// `connect` is simply a delegate defined inside +// `GameRules_OnNetDamage_Slot` object +_.unreal.gameRules.OnNetDamage(self).connect = NetDamage; + +``` + +If returned signal is not assigned any function, then it will be automatically +cleaned up at some later point. +We cannot directly check whether a `delegate` was assigned some value, but +`connect`'s default implementation calls special protected method `DummyCall()` +that tells us that its slot is empty. + +### Disconnecting + +So how does `_.unreal.gameRules.OnNetDamage(someObj).Disconnect()` work? +Same as above, `_.unreal.gameRules.OnNetDamage(someObj)` creates a new empty +slot associated with `someObj`. +This slot is aware of both `signal` its connected to and associated `someObj`. +`Disconnect()` method makes our `slot` inform its `signal` that all `slot`s +related to `someObj` (including itself) must be disconnected and deallocated. + +This means that disconnecting object's `slot`s from the `signal` with +`Disconnect()`always involves creation of the new `slot` that will never be +connected to any handler method. +It is a roundabout way of doing things, but it provides a simple interface for +the task that shouldn't be performed often enough to affect performance. + +## [Advanced] How to make your own `signal`s and `slot`s + +Providing support for your own signals and slots actually takes quite a bit more +work that using them. +Here we will consider main use cases. + +### Simple notification events + +If you need to add an event with handlers that don't take any parameters and +don't return anything, then easiest way it to use `SimpleSignal` / `SingleSlot` +classes: + +```unrealscript +class MyEventClass extends AcediaObject; + +var private SimpleSignal onMyEventSignal; + +protected function Constructor() +{ + onMyEventSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal')); +} + +protected function Finalizer() +{ + _.memory.Free(onMyEventSignal); + onMyEventSignal = none; +} + +public function SimpleSlot OnMyEvent(AcediaObject receiver) +{ + return SimpleSlot(onMyEventSignal.NewSlot(receiver)); +} + +// Suppose you want to emit the signal when this function is called... +public function SimpleSlot FireOffMyEvent(AcediaObject receiver) +{ + // ...simply call this and all the slots will have their handlers called + onMyEventSignal.Emit(); +} +``` + +Then you can use `OnMyEvent()` as a *signal function*: + +```unrealscript +// To add handlers +myEventClassInstance.OnMyEvent(self).connect = handler; +// To remove handlers +myEventClassInstance.OnMyEvent(self).Disconnect(); +``` + +### Events with parameters + +Some of the events, like `OnNetDamage()` in our first examples, can take +parameters. +We cannot use `SimpleSignal` or `SimpleSlot` for them and have to define new +classes that will wrap around `delegate` with an appropriate signature. + +You simply need to follow the template and define new classes like this: + +```unrealscript +class MySignal extends Signal; + +public final function Emit() +{ + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + MySlot(nextSlot).connect(); + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); +} + +defaultproperties +{ + relatedSlotClass = class'MySlot' +} +``` + +```unrealscript +class MySlot extends Slot; + +delegate connect() +{ + DummyCall(); // This allows Acedia to cleanup slots without set handlers +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} + +defaultproperties +{ +} +``` + +where you can use any set of parameters instead of ``. +You can check out `Unreal_OnTick_Signal` and `Unreal_OnTick_Slot` as +an example. + +### Events with return values + +Sometimes you want your handlers to respond in some way to the event. +You can either allow them to modify input parameters (e.g. by declaring them as +`out`) or allow them to have return value. +`OnNetDamage()`, for example, is allowed to modify incoming damage by returning +a new value. + +To add signals / slots that handle return value use following templates: + +```unrealscript +class MySignal extends Signal; + +public final function Emit() +{ + local newValue; + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + newValue = (nextSlot).connect(); + // This check if necessary before using returned value + if (!nextSlot.IsEmpty()) + { + // Now handle `newValue` however you see fit + } + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); + // Return whatever you see fit after handling all the slots + return ; +} + +defaultproperties +{ + relatedSlotClass = class'MySlot' +} +``` + +```unrealscript +class MySlot extends Slot; + +delegate connect() +{ + DummyCall(); + // Return anything you want: + // this value will be filtered inside corresponding `Signal` + // if no handler is set to the associated slot + return ; +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} +``` + +For working example you can check out `GameRules_OnNetDamage_Signal` and +`GameRules_OnNetDamage_Slot` classes. + +## [Advanced] How to remove particular `slot` + +In our very first example we've seen that we can remove all `slot`s for +`OnNetDamage()`, associated with `someObj` by calling +`_.unreal.gameRules.OnNetDamage(someObj).Disconnect()`. +But sometimes it might be necessary to remove only one slot. +In that case you'll have to store that slot in a separate variable: + +```unrealscript +// Each `signal` can have its own `slot` type +var GameRules_OnNetDamage_Slot trackedSlot; +var int trackedSlotLifeVersion; +// ... +// Record new `slot` in a variable, then set `connect` delegate +trackedSlot = _.unreal.gameRules.OnNetDamage(self); +trackedSlot.connect = handler; +trackedSlotLifeVersion = trackedSlot.GetLifeVersion(); +// ... +// 1. You can then change `slot`'s handler +if (trackedSlotLifeVersion == trackedSlot.GetLifeVersion()) { + trackedSlot.connect = handler2; +} +// ... +// 2. Or deallocate `slot` once it's no longer needed - +// its handler won't be called again +trackedSlot.FreeSelf(trackedSlotLifeVersion); +trackedSlot = none; +``` + +Here we record *life version* because it is `signal` and not us that is +responsible for the deallocation of `slot`s. +If we don't check life versions, we might use `slot` reallocated for a different +purpose. +This shouldn't happen unless you deallocate `trackedSlot` in some other way +(e.g. with `_.unreal.gameRules.OnNetDamage(self).Disconnect()`), but its safer +to do this check. +Not accessing separate `slot`s is even safer. + +> **NOTE:** +> `singal`s themselves also track `slot`'s life versions and will be able to +> tell if you've deallocated them. diff --git a/docs/index.md b/docs/index.md index 2afc53a..92e3aa1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -51,4 +51,4 @@ about any topic of interest, but we strongly recommend that you first read up on the fundamental topics: [what is API](./api.md), at least non-advanced topics of [Acedia's objects / actors](./objects.md) -and about signal / slot system. +and about [signal / slot system](./events.md).