/**
* One of the two classes that make up a core of event system in Acedia.
* `Signal`s, along with `Slot`s, are used for communication between
* objects. Signals can be connected to slots of appropriate class and emitted.
* When a signal is emitted, all connected slots are notified and their handler
* is called.
* This `Signal`-`Slot` system is essentially a wrapper for delegates
* (`Slot` wraps over a single delegate, allowing us to store them in array),
* but, unlike them, makes it possible to add several handlers for any event in
* a convenient to use way, e.g.:
* `_.unreal.OnTick(self).connect = myTickHandler`
* To create your own `Signal` you need to:
* 1. Make a non-abstract child class of `Signal`;
* 2. Use one of the templates presented in this file below;
* 3. Create a paired `Slot` class and set it's class to `relatedSlotClass`
* in `defaultproperties`.
* 4. (Recommended) Provide a standard interface by defining an event
* method (similar to `_.unreal.OnTick()`) in an object that will own
* this signal, example of definition is also listed below.
* More detailed information can be found in documentation.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
class Signal extends AcediaObject
abstract;
/**
* `Signal` essentially has to provide functionality for
* connecting/disconnecting slots and iterating through them. The main
* challenge is that slots can also be connected / disconnected during
* the signal emission. And in such cases we want:
* 1. To not propagate a signal to `Slot`s that were added during
* it's emission;
* 2. To not propagate a signal to any removed `Slot`, even if it was
* connected to the `Signal` in question when signal started emitting.
*
* We store connected `Slot`s in array, so to iterate we will simply use
* internal index variable `nextSlotIndex`. To account for removal of `Slot`s
* we will simply have to appropriately correct `nextSlotIndex` variable.
* To account for adding `Slot`s during signal emission we will first add them
* to a temporary queue `slotQueueToAdd` and only dump slots stored there
* into actual connected `Slot`s array before next iteration starts.
*/
// Class of the slot that can catch your `Signal` class
var public const class relatedSlotClass;
// Set to `true` when we are in the process of removing connected `Slot`s.
// Once `Slot` is deallocated, it notifies it's `Signal` (us) that it
// should be removed.
// But if it's deallocated because we are removing it, then we want to
// ignore that notification and this flag helps us do that.
var private bool doingSelfCleaning;
// Used to provide iterating interface (`StartIterating()` / `GetNextSlot()`).
// Points at the next slot to return.
var private int nextSlotIndex;
// This record describes slot-receiver pair to be added, along with it's
// life versions at the moment of adding a slot. Life versions help us verify
// that slot/receiver were not re-allocated at some point
// (thus becoming different objects).
struct SlotRecord
{
var Slot slotInstance;
var int slotLifeVersion;
var AcediaObject receiver;
var int receiverLifeVersion;
};
// Slots to be added before the next iteration (signal emission).
// We ensure that any added record has `slotInstance != none`.
var array slotQueueToAdd;
// These arrays could be defined as one array of `SlotRecord` structs.
// We use four different arrays instead for performance reasons.
// (Acedia is expected to make extensive use of `Signal`s and `Slot`s, so it's
// reasonable to consider even small optimization in this case).
// They must have the same length at all times and elements with the
// same index correspond to the same "record".
// References to registered `Slot`s
var private array registeredSlots;
// Life versions of the registered `Slot`s, to track unexpected deallocations
var private array slotLifeVersions;
// Receivers, associated with the `Slot`s: when they're deallocated,
// corresponding `Slot`s should be removed
var private array slotReceivers;
// Life versions of the registered receivers, to track their deallocation
var private array slotReceiversLifeVersions;
/* TEMPLATE for handlers without returned values:
public final function Emit()
{
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
(nextSlot).connect();
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
*/
/* TEMPLATE for handlers with returned values:
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 ;
}
*/
/* TEMPLATE for the interface method:
var private mySignal;
public final function OnMyEvent(AcediaObject receiver)
{
return (mySignal.NewSlot(receiver));
}
*/
protected function Finalizer()
{
local int i;
doingSelfCleaning = true;
// Free queue for slot addition
for (i = 0; i < slotQueueToAdd.length; i += 1)
{
slotQueueToAdd[i].slotInstance
.FreeSelf(slotQueueToAdd[i].slotLifeVersion);
}
slotQueueToAdd.length = 0;
// Free actually connected slots
for (i = 0; i < registeredSlots.length; i += 1) {
registeredSlots[i].FreeSelf(slotLifeVersions[i]);
}
doingSelfCleaning = false;
registeredSlots.length = 0;
slotLifeVersions.length = 0;
slotReceivers.length = 0;
slotReceiversLifeVersions.length = 0;
}
/**
* Creates a new slot for `receiver` to catch emitted signals.
* Supposed to be used inside a special interface method only.
*
* @param receiver Receiver to which new `Slot` would be connected to.
* Method connected to a `Slot` generated by this method must belong to
* the `receiver`, otherwise behavior of `Signal`-`Slot` system is
* undefined.
* Must be a properly allocated `AcediaObject`.
* @return New `Slot` object that will be connected to the caller `Signal` if
* provided `receiver` is correct. Guaranteed to have class
* `relatedSlotClass`.
*/
public final function Slot NewSlot(AcediaObject receiver)
{
local Slot newSlot;
newSlot = Slot(_.memory.Allocate(relatedSlotClass));
newSlot.Initialize(self, receiver);
AddSlot(newSlot, receiver);
return newSlot;
}
/**
* Disconnects all of the `receiver`'s `Slot`s from the caller `Signal`.
*
* Meant to only be used by the `Slot`s `Disconnect()` method.
*
* @param receiver Object to disconnect from the caller `Signal`.
* If `none` is passed, does nothing.
*/
public final function Disconnect(AcediaObject receiver)
{
local int i;
if (receiver == none) {
return;
}
doingSelfCleaning = true;
// Clean from the queue for addition
i = 0;
while (i < slotQueueToAdd.length)
{
if (slotQueueToAdd[i].receiver == receiver)
{
slotQueueToAdd[i].slotInstance
.FreeSelf(slotQueueToAdd[i].slotLifeVersion);
slotQueueToAdd.Remove(i, 1);
}
else {
i += 1;
}
}
// Clean from the active slots
i = 0;
while (i < slotReceivers.length)
{
if (slotReceivers[i] == receiver) {
RemoveSlotAtIndex(i);
}
else {
i += 1;
}
}
doingSelfCleaning = false;
}
/**
* Adds new `Slot` (`newSlot`) with receiver `receiver` to the caller `Signal`.
*
* Won't affect caller `Signal` if `newSlot` is already added to it
* (even if it's added with a different receiver).
*
* @param newSlot Slot to add. Must be initialize for the caller `Signal`.
* @param receiver Receiver to which new `Slot` would be connected.
* Method connected to a `Slot` generated by this method must belong to
* the `receiver`, otherwise behavior of `Signal`-`Slot` system is
* undefined. Must be a properly allocated `AcediaObject`.
*/
protected final function AddSlot(Slot newSlot, AcediaObject receiver)
{
local SlotRecord newRecord;
// Do not check whether `receiver` is `none`, this requires handling
// `newSlot`'s deallocation and it will be dealt with at the moment of
// adding new slots from `slotQueueToAdd` queue to the caller `Signal`.
// This situation should not normally occur in the first place, so
// it does not matter if the `slotQueueToAdd` grows larger than needed when
// this does happen.
if (newSlot == none) {
return;
}
newRecord.slotInstance = newSlot;
newRecord.slotLifeVersion = newSlot.GetLifeVersion();
newRecord.receiver = receiver;
if (receiver != none) {
newRecord.receiverLifeVersion = receiver.GetLifeVersion();
}
slotQueueToAdd[slotQueueToAdd.length] = newRecord;
}
// Attempts to add a `Slot` from a `SlotRecord` into array of currently
// connected `Slot`s.
// IMPORTANT: Must only be called right before a new iteration
// (signal emission) through the `Slot`s. Otherwise `Signal`'s behavior
// should be considered undefined.
private final function AddSlotRecord(SlotRecord record)
{
local int i;
local int newSlotIndex;
local Slot newSlot;
local AcediaObject receiver;
newSlot = record.slotInstance;
receiver = record.receiver;
if (newSlot.class != relatedSlotClass) return;
if (!newSlot.IsOwnerSignal(self)) return;
// Slot got deallocated while waiting in queue
if (newSlot.GetLifeVersion() != record.slotLifeVersion) return;
// Receiver is outright invalid or got deallocated
if ( receiver == none
|| !receiver.IsAllocated()
|| receiver.GetLifeVersion() != record.receiverLifeVersion)
{
doingSelfCleaning = true;
newSlot.FreeSelf();
doingSelfCleaning = false;
return;
}
// Check if that slot is already added
for (i = 0; i < registeredSlots.length; i += 1)
{
if (registeredSlots[i] != newSlot) {
continue;
}
// If we have the same instance recorded, but...
// 1. it was reallocated: update it's records;
// 2. it was not reallocated: leave the records intact.
// Neither case would cause issues with iterating along `Slot`s if this
// method is only called right before new iteration through `Slot`s.
if (slotLifeVersions[i] != record.slotLifeVersion)
{
slotLifeVersions[i] = record.slotLifeVersion;
slotReceivers[i] = receiver;
if (receiver != none) {
slotReceiversLifeVersions[i] = record.receiverLifeVersion;
}
}
return;
}
newSlotIndex = registeredSlots.length;
registeredSlots[newSlotIndex] = newSlot;
slotLifeVersions[newSlotIndex] = record.slotLifeVersion;
slotReceivers[newSlotIndex] = receiver;
slotReceiversLifeVersions[newSlotIndex] = record.receiverLifeVersion;
}
/**
* Removes given `slotToRemove` if it was connected to the caller `Signal`.
*
* Cannot fail.
*
* @param slotToRemove Slot to be removed.
*/
public final function RemoveSlot(Slot slotToRemove)
{
local int i;
if (slotToRemove == none) return;
if (doingSelfCleaning) return;
// Remove from queue for addition
while (i < slotQueueToAdd.length)
{
if (slotQueueToAdd[i].slotInstance == slotToRemove)
{
slotToRemove.FreeSelf(slotQueueToAdd[i].slotLifeVersion);
slotQueueToAdd.Remove(i, 1);
}
else {
i += 1;
}
}
// Remove from active slots
for (i = 0; i < registeredSlots.length; i += 1)
{
if (registeredSlots[i] == slotToRemove)
{
RemoveSlotAtIndex(i);
return;
}
}
}
/**
* One of two methods that provide an iterator access to the private array of
* `Slot`s and perform all the necessary safety checks.
*
* Must be called before each new iterating cycle.
*
* There cannot be any `Slot` additions or removal during one iteration cycle.
*/
protected final function StartIterating()
{
local int i;
for (i = 0; i < slotQueueToAdd.length; i += 1) {
AddSlotRecord(slotQueueToAdd[i]);
}
slotQueueToAdd.length = 0;
nextSlotIndex = 0;
}
/**
* One of two methods that provide an iterator access to the private array of
* `Slot`s and perform all the necessary safety checks.
*
* `StartIterating()` must be called to initialize iteration cycle, then this
* method can be called until it returns `none`.
*
* There cannot be any `Slot` additions or removal during one iteration cycle.
*
* @return Next `Slot` that must receive emitted signal. `none` means that
* there are no more `Slot`s to iterate over.
*/
protected final function Slot GetNextSlot()
{
local bool isNextSlotValid;
local int nextSlotLifeVersion, nextReceiverLifeVersion;
local Slot nextSlot;
local AcediaObject nextReceiver;
doingSelfCleaning = true;
while (nextSlotIndex < registeredSlots.length)
{
nextSlot = registeredSlots[nextSlotIndex];
nextSlotLifeVersion = slotLifeVersions[nextSlotIndex];
nextReceiver = slotReceivers[nextSlotIndex];
nextReceiverLifeVersion = slotReceiversLifeVersions[nextSlotIndex];
isNextSlotValid = (nextSlot.GetLifeVersion() == nextSlotLifeVersion)
&& (nextReceiver.GetLifeVersion() == nextReceiverLifeVersion);
if (isNextSlotValid)
{
nextSlotIndex += 1;
doingSelfCleaning = false;
return nextSlot;
}
else {
RemoveSlotAtIndex(nextSlotIndex);
}
}
doingSelfCleaning = false;
return none;
}
/**
* In case it's detected that some of the slots do not actually have any
* delegate set - this method will clean them up.
*/
protected final function CleanEmptySlots()
{
local int index;
doingSelfCleaning = true;
while (index < registeredSlots.length)
{
if (registeredSlots[index].IsEmpty()) {
RemoveSlotAtIndex(index);
}
else {
index += 1;
}
}
doingSelfCleaning = false;
}
// Removes `Slot` at a given `index`.
// Assumes that passed index is within boundaries.
private final function RemoveSlotAtIndex(int index)
{
registeredSlots[index].FreeSelf(slotLifeVersions[index]);
registeredSlots.Remove(index, 1);
slotLifeVersions.Remove(index, 1);
slotReceivers.Remove(index, 1);
slotReceiversLifeVersions.Remove(index, 1);
// Alter iteration index `nextSlotIndex` to account for this `Slot` removal
if (nextSlotIndex > index) {
nextSlotIndex -= 1;
}
}
defaultproperties
{
relatedSlotClass = class'Slot'
}