diff --git a/sources/Events/Signal.uc b/sources/Events/Signal.uc
new file mode 100644
index 0000000..3f683d1
--- /dev/null
+++ b/sources/Events/Signal.uc
@@ -0,0 +1,347 @@
+/**
+ * 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;
+
+// Class of the slot that can catch your `Signal` class
+var public const class relatedSlotClass;
+
+// We want to always return a non-`none` slot to avoid "Access 'none'"
+// errors.
+// So if provided signal receiver is invalid - we still create a `Slot`
+// for it, but remember it in this array of slots we aren't going to use
+// in order to dispose of them later.
+var private array failedSlots;
+
+// Set to `true` when we are in the process of deleting our `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 we want to ignore
+// their 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;
+
+// These arrays could be defined as one array of `struct`s with four
+// elements.
+// We use four different arrays instead for performance reasons.
+// They must have the same length at all times and elements with the
+// same index correspond to the same "record".
+
+// Reference to registered `Slot`
+var private array registeredSlots;
+// Life version of the registered `Slot`, to track unexpected deallocations
+var private array slotLifeVersions;
+// Receiver, associated with the `Slot`: when it's deallocated,
+// corresponding `Slot` should be removed
+var private array slotReceivers;
+// Life version of the registered receiver, to track it's 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 int 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 value;
+}
+*/
+
+/* TEMPLATE for the interface method:
+
+var private mySignal;
+public final function OnMyEvent(AcediaObject receiver)
+{
+ return (mySignal.NewSlot(receiver));
+}
+*/
+
+protected function Finalizer()
+{
+ doingSelfCleaning = true;
+ _.memory.FreeMany(registeredSlots);
+ 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.
+ * 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);
+ AddSlot(newSlot, receiver);
+ return newSlot;
+}
+
+/**
+ * Disconnects all of the `receiver`'s `Slot`s from the caller `Signal`.
+ *
+ * @param receiver Object to disconnect from the caller `Signal`.
+ */
+public final function Disconnect(AcediaObject receiver)
+{
+ local int i;
+ doingSelfCleaning = true;
+ while (i < slotReceivers.length)
+ {
+ if (slotReceivers[i] == none || slotReceivers[i] == receiver)
+ {
+ _.memory.Free(registeredSlots[i]);
+ registeredSlots.Remove(i, 1);
+ slotLifeVersions.Remove(i, 1);
+ slotReceivers.Remove(i, 1);
+ slotReceiversLifeVersions.Remove(i, 1);
+ }
+ else {
+ i += 1;
+ }
+ }
+ doingSelfCleaning = false;
+}
+
+/**
+ * Adds new `Slot` `newSlot` with receiver `receiver` to the caller `Signal`.
+ *
+ * Does nothing if `newSlot` is already added to the caller `Signal`.
+ *
+ * @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 int i;
+ local int newSlotIndex;
+ if (newSlot == none) return;
+ if (newSlot.class != relatedSlotClass) return;
+ if (!newSlot.IsOwnerSignal(self)) return;
+ if (receiver == none || !receiver.IsAllocated())
+ {
+ failedSlots[failedSlots.length] = newSlot;
+ return;
+ }
+ for (i = 0; i < registeredSlots.length; i += 1)
+ {
+ if (registeredSlots[i] != newSlot) {
+ continue;
+ }
+ if (slotLifeVersions[i] != newSlot.GetLifeVersion())
+ {
+ slotLifeVersions[i] = newSlot.GetLifeVersion();
+ slotReceivers[i] = receiver;
+ if (receiver != none) {
+ slotReceiversLifeVersions[i] = receiver.GetLifeVersion();
+ }
+ }
+ return;
+ }
+ newSlotIndex = registeredSlots.length;
+ registeredSlots[newSlotIndex] = newSlot;
+ slotLifeVersions[newSlotIndex] = newSlot.GetLifeVersion();
+ slotReceivers[newSlotIndex] = receiver;
+ slotReceiversLifeVersions[newSlotIndex] = receiver.GetLifeVersion();
+}
+
+/**
+ * Removes given `slotToRemove` if it was connected to the caller `Signal`.
+ *
+ * Does not deallocate `slotToRemove`.
+ *
+ * Cannot fail.
+ *
+ * @param slotToRemove Slot to be removed.
+ */
+public final function RemoveSlot(Slot slotToRemove)
+{
+ local int i;
+ if (slotToRemove == none) return;
+ if (doingSelfCleaning) return;
+
+ for (i = 0; i < registeredSlots.length; i += 1)
+ {
+ if (registeredSlots[i] == slotToRemove)
+ {
+ registeredSlots.Remove(i, 1);
+ slotLifeVersions.Remove(i, 1);
+ slotReceivers.Remove(i, 1);
+ slotReceiversLifeVersions.Remove(i, 1);
+ 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()
+{
+ 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;
+ return nextSlot;
+ }
+ else
+ {
+ registeredSlots.Remove(nextSlotIndex, 1);
+ slotLifeVersions.Remove(nextSlotIndex, 1);
+ slotReceivers.Remove(nextSlotIndex, 1);
+ slotReceiversLifeVersions.Remove(nextSlotIndex, 1);
+ _.memory.Free(nextSlot);
+ }
+ }
+ doingSelfCleaning = false;
+ return none;
+}
+
+/**
+ * In case it's detected that some of the slots do not actually have any
+ * handler setup - this method will clean them up.
+ */
+protected final function CleanEmptySlots()
+{
+ local int index;
+ _.memory.FreeMany(failedSlots);
+ failedSlots.length = 0;
+ doingSelfCleaning = true;
+ while (index < registeredSlots.length)
+ {
+ if (registeredSlots[index].IsEmpty())
+ {
+ registeredSlots[index].FreeSelf(slotLifeVersions[index]);
+ _.memory.Free(registeredSlots[index]);
+ registeredSlots.Remove(index, 1);
+ slotLifeVersions.Remove(index, 1);
+ slotReceivers.Remove(index, 1);
+ slotReceiversLifeVersions.Remove(index, 1);
+ }
+ else {
+ index += 1;
+ }
+ }
+ doingSelfCleaning = false;
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'Slot'
+}
\ No newline at end of file
diff --git a/sources/Events/Slot.uc b/sources/Events/Slot.uc
new file mode 100644
index 0000000..2a72ce6
--- /dev/null
+++ b/sources/Events/Slot.uc
@@ -0,0 +1,151 @@
+/**
+ * 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 `Slot` you need to:
+ * 1. Make a non-abstract child class of `Signal`;
+ * 2. Use one of the templates presented in this file 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 Slot extends AcediaObject
+ abstract;
+
+var private bool dummyMethodCalled;
+var private Signal ownerSignal;
+
+/* TEMPLATE for handlers without returned values:
+delegate connect()
+{
+ DummyCall();
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+*/
+
+/* TEMPLATE for handlers with returned values:
+delegate connect()
+{
+ DummyCall();
+ // Return anything you want:
+ // this value will be filtered inside corresponding `Signal`
+ return ??>;
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+*/
+
+protected function Finalizer()
+{
+ dummyMethodCalled = false;
+ if (ownerSignal != none) {
+ ownerSignal.RemoveSlot(self);
+ }
+ ownerSignal = none;
+}
+
+/**
+ * Calling this method marks caller `Slot` as "empty", i.e. having an empty
+ * delegate. `Slot`s like that are deleted from `Signal`s upon detection.
+ *
+ * Must be called inside your `connect()` implementation.
+ */
+protected final function DummyCall()
+{
+ dummyMethodCalled = true;
+ // We do not want to call `ownerSignal.RemoveSlot(self)` here, since
+ // `ownerSignal` is likely in process of iterating through it's `Slot`s
+ // and removing (or adding) `Slot`s from it can mess up that process.
+}
+
+/**
+ * Initialized caller `Slot` to receive signals emitted by `newOwnerSignal`.
+ *
+ * Can only be done once for every `Slot`.
+ *
+ * @param newOwnerSignal `Signal` we want to receive emitted signals from.
+ * @return `true` if initialization was successful and `false` otherwise
+ * (if `newOwnerSignal` is invalid or caller `Slot` was
+ * already initialized).
+ */
+public final function bool Initialize(Signal newOwnerSignal)
+{
+ if (ownerSignal != none) {
+ return false;
+ }
+ if (newOwnerSignal == none || !newOwnerSignal.IsAllocated())
+ {
+ FreeSelf();
+ return false;
+ }
+ ownerSignal = newOwnerSignal;
+ return true;
+}
+
+/**
+ * Checks if caller `Slot` was initialized to receive `testSignal`'s signals.
+ *
+ * @param testSignal `Signal` to test.
+ * @return `true` if caller `Slot` was initialized to receiver `testSignal`'s
+ * signals and `false` otherwise.
+ */
+public final function bool IsOwnerSignal(Signal testSignal)
+{
+ return (ownerSignal == testSignal);
+}
+
+/**
+ * Checks if caller `Slot` was detected to be "empty", i.e. having an empty
+ * delegate. `Slot`s like that are deleted from `Signal`s upon detection.
+ *
+ * @return `true` if caller `Slot` is empty (and should be removed from the
+ * appropriate `Signal`) and `false` otherwise.
+ */
+public final function bool IsEmpty()
+{
+ return dummyMethodCalled;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Events/Tests/MockSignal.uc b/sources/Events/Tests/MockSignal.uc
new file mode 100644
index 0000000..5b872cb
--- /dev/null
+++ b/sources/Events/Tests/MockSignal.uc
@@ -0,0 +1,43 @@
+/**
+ * `Signal` class intended for testing signal/slot functionality of Acedia.
+ * 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 MockSignal extends Signal;
+
+public final function int Emit(int value, optional bool mod)
+{
+ local int newValue;
+ local Slot nextSlot;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ newValue = MockSlot(nextSlot).connect(value, mod);
+ if (!nextSlot.IsEmpty()) {
+ value = newValue;
+ }
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+ return value;
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'MockSlot'
+}
\ No newline at end of file
diff --git a/sources/Events/Tests/MockSignalSlotClient.uc b/sources/Events/Tests/MockSignalSlotClient.uc
new file mode 100644
index 0000000..dbfdf69
--- /dev/null
+++ b/sources/Events/Tests/MockSignalSlotClient.uc
@@ -0,0 +1,49 @@
+/**
+ * Object that provides a signal handler for testing signal/slot functionality
+ * of Acedia.
+ * 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 MockSignalSlotClient extends AcediaObject;
+
+var private int value;
+
+public final function SetValue(int newValue)
+{
+ value = newValue;
+}
+
+// Return `SMockSlot` for testing purposes
+public final function MockSlot AddToSignal(MockSignal signal)
+{
+ local MockSlot slot;
+ slot = MockSlot(signal.NewSlot(self));
+ slot.connect = Handler;
+ return slot;
+}
+
+private final function int Handler(int inputValue, optional bool doSubtract)
+{
+ if (doSubtract) {
+ return inputValue - value;
+ }
+ return inputValue + value;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Events/Tests/MockSlot.uc b/sources/Events/Tests/MockSlot.uc
new file mode 100644
index 0000000..874f8f7
--- /dev/null
+++ b/sources/Events/Tests/MockSlot.uc
@@ -0,0 +1,41 @@
+/**
+ * `Slot` class intended for testing signal/slot functionality of Acedia.
+ * 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 MockSlot extends Slot;
+
+delegate int connect(int value, optional bool mod)
+{
+ DummyCall();
+ return 13;
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Events/Tests/TEST_SignalsSlots.uc b/sources/Events/Tests/TEST_SignalsSlots.uc
new file mode 100644
index 0000000..6920b6c
--- /dev/null
+++ b/sources/Events/Tests/TEST_SignalsSlots.uc
@@ -0,0 +1,214 @@
+/**
+ * Set of tests for signal/slot functionality of Acedia.
+ * 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 TEST_SignalsSlots extends TestCase
+ abstract;
+
+protected static function TESTS()
+{
+ Context("Testing regularly connecting and disconnecting slots to"
+ @ "a signal.");
+ Test_Connecting();
+ Test_Disconnecting();
+ Context("Testing how signals and slots system handles deallocations and"
+ @ "unexpected changes to managed objects.");
+ Test_DeallocSlots();
+ Test_EmptySlots();
+ Test_DeallocReceivers();
+}
+
+protected static function Test_Connecting()
+{
+ local int i;
+ local MockSignal signal;
+ local MockSignalSlotClient nextObject;
+ local array objects;
+ Issue("Slots are not connected correctly.");
+ signal = MockSignal(__().memory.Allocate(class'MockSignal'));
+ for (i = 0; i < 100; i += 1)
+ {
+ nextObject = MockSignalSlotClient(
+ __().memory.Allocate(class'MockSignalSlotClient'));
+ objects[objects.length] = nextObject;
+ nextObject.AddToSignal(signal);
+ nextObject.SetValue(i + 1);
+ }
+ // 1 + ... + 100 = 100 * 101 / 2 = 5050
+ // 5050 + 100 = 5150
+ TEST_ExpectTrue(signal.Emit(100) == 5150);
+ // -5050 + 20 = -5030
+ TEST_ExpectTrue(signal.Emit(20, true) == -5030);
+ __().memory.Free(signal);
+
+ Issue("Several slots from the same object are not connected correctly.");
+ signal = MockSignal(__().memory.Allocate(class'MockSignal'));
+ // Object with `SetValue(100)`
+ nextObject.AddToSignal(signal);
+ nextObject.AddToSignal(signal);
+ nextObject.AddToSignal(signal);
+ TEST_ExpectTrue(signal.Emit(400, true) == 100);
+ __().memory.FreeMany(objects);
+}
+
+protected static function Test_Disconnecting()
+{
+ local int i;
+ local MockSignal signal;
+ local MockSignalSlotClient nextObject;
+ local array objects;
+ Issue("Slots are not disconnected correctly.");
+ signal = MockSignal(__().memory.Allocate(class'MockSignal'));
+ for (i = 0; i < 100; i += 1)
+ {
+ nextObject = MockSignalSlotClient(
+ __().memory.Allocate(class'MockSignalSlotClient'));
+ objects[objects.length] = nextObject;
+ nextObject.AddToSignal(signal);
+ nextObject.SetValue(i + 1);
+ }
+ // Now disconnect the ones with odd values
+ for (i = 0; i < 100; i += 2) {
+ signal.Disconnect(objects[i]); // value is `i + 1`, so 1, 3, 5,...
+ }
+ // 2 + 4 + 6 + ... + 100 = 2 * (1 + ... + 50) = 50 * 51 = 2550
+ // 2550 + 50 = 2600
+ TEST_ExpectTrue(signal.Emit(50) == 2600);
+ // -2550 + 550 = -2000
+ TEST_ExpectTrue(signal.Emit(550, true) == -2000);
+ __().memory.Free(signal);
+ __().memory.FreeMany(objects);
+}
+
+protected static function Test_DeallocSlots()
+{
+ local int i;
+ local MockSignal signal;
+ local MockSignalSlotClient nextObject;
+ local array slots;
+ local array objects;
+ Issue("Deallocated slots are still being called.");
+ signal = MockSignal(__().memory.Allocate(class'MockSignal'));
+ for (i = 0; i < 100; i += 1)
+ {
+ nextObject = MockSignalSlotClient(
+ __().memory.Allocate(class'MockSignalSlotClient'));
+ objects[objects.length] = nextObject;
+ slots[slots.length] = nextObject.AddToSignal(signal);
+ nextObject.SetValue(i + 1);
+ }
+ // Now disconnect the ones with odd values
+ for (i = 0; i < 100; i += 2) {
+ slots[i].FreeSelf(); // value is `i + 1`, so 1, 3, 5,...
+ }
+ // 2 + 4 + 6 + ... + 100 = 2 * (1 + ... + 50) = 50 * 51 = 2550
+ // 2550 + 50 = 2600
+ TEST_ExpectTrue(signal.Emit(50) == 2600);
+ // -2550 + 550 = -2000
+ TEST_ExpectTrue(signal.Emit(550, true) == -2000);
+ __().memory.Free(signal);
+ __().memory.FreeMany(objects);
+}
+
+protected static function Test_EmptySlots()
+{
+ local int i;
+ local bool slotsAreNotDeallocated;
+ local MockSignal signal;
+ local MockSignalSlotClient nextObject;
+ local array slots;
+ local array objects;
+ Issue("Slots with emptied delegates are still being called.");
+ signal = MockSignal(__().memory.Allocate(class'MockSignal'));
+ for (i = 0; i < 100; i += 1)
+ {
+ nextObject = MockSignalSlotClient(
+ __().memory.Allocate(class'MockSignalSlotClient'));
+ objects[objects.length] = nextObject;
+ slots[slots.length] = nextObject.AddToSignal(signal);
+ nextObject.SetValue(i + 1);
+ }
+ // Now disconnect the ones with odd values
+ for (i = 0; i < 100; i += 2) {
+ slots[i].connect = none; // value is `i + 1`, so 1, 3, 5,...
+ }
+ // 2 + 4 + 6 + ... + 100 = 2 * (1 + ... + 50) = 50 * 51 = 2550
+ // 2550 + 50 = 2600
+ TEST_ExpectTrue(signal.Emit(50) == 2600);
+ // -2550 + 550 = -2000
+ TEST_ExpectTrue(signal.Emit(550, true) == -2000);
+ for (i = 0; i < 100; i += 2)
+ {
+ if (slots[i].IsAllocated())
+ {
+ slotsAreNotDeallocated = true;
+ break;
+ }
+ }
+ Issue("Slots with emptied delegates are not deallocated.");
+ TEST_ExpectFalse(slotsAreNotDeallocated);
+ __().memory.Free(signal);
+ __().memory.FreeMany(objects);
+}
+
+protected static function Test_DeallocReceivers()
+{
+ local int i;
+ local bool slotsAreNotDeallocated;
+ local MockSignal signal;
+ local MockSignalSlotClient nextObject;
+ local array slots;
+ local array objects;
+ Issue("Deallocated receivers still receive messages.");
+ signal = MockSignal(__().memory.Allocate(class'MockSignal'));
+ for (i = 0; i < 100; i += 1)
+ {
+ nextObject = MockSignalSlotClient(
+ __().memory.Allocate(class'MockSignalSlotClient'));
+ objects[objects.length] = nextObject;
+ slots[slots.length] = nextObject.AddToSignal(signal);
+ nextObject.SetValue(i + 1);
+ }
+ // Now disconnect the ones with odd values
+ for (i = 0; i < 100; i += 2) {
+ objects[i].FreeSelf(); // value is `i + 1`, so 1, 3, 5,...
+ }
+ // 2 + 4 + 6 + ... + 100 = 2 * (1 + ... + 50) = 50 * 51 = 2550
+ // 2550 + 50 = 2600
+ TEST_ExpectTrue(signal.Emit(50) == 2600);
+ // -2550 + 550 = -2000
+ TEST_ExpectTrue(signal.Emit(550, true) == -2000);
+ for (i = 0; i < 100; i += 2)
+ {
+ if (slots[i].IsAllocated())
+ {
+ slotsAreNotDeallocated = true;
+ break;
+ }
+ }
+ Issue("Slots with deallocated receivers are not deallocated.");
+ TEST_ExpectFalse(true);
+ __().memory.Free(signal);
+ __().memory.FreeMany(objects);
+}
+
+defaultproperties
+{
+ caseGroup = "Events"
+ caseName = "Signals and slots"
+}
\ No newline at end of file
diff --git a/sources/Manifest.uc b/sources/Manifest.uc
index 4cb36f0..7b60c6c 100644
--- a/sources/Manifest.uc
+++ b/sources/Manifest.uc
@@ -34,21 +34,22 @@ defaultproperties
testCases(0) = class'TEST_Base'
testCases(1) = class'TEST_Boxes'
testCases(2) = class'TEST_Refs'
- testCases(3) = class'TEST_UnrealAPI'
- testCases(4) = class'TEST_Aliases'
- testCases(5) = class'TEST_ColorAPI'
- testCases(6) = class'TEST_Text'
- testCases(7) = class'TEST_TextAPI'
- testCases(8) = class'TEST_Parser'
- testCases(9) = class'TEST_JSON'
- testCases(10) = class'TEST_TextCache'
- testCases(11) = class'TEST_User'
- testCases(12) = class'TEST_Memory'
- testCases(13) = class'TEST_DynamicArray'
- testCases(14) = class'TEST_AssociativeArray'
- testCases(15) = class'TEST_CollectionsMixed'
- testCases(16) = class'TEST_Iterator'
- testCases(17) = class'TEST_Command'
- testCases(18) = class'TEST_CommandDataBuilder'
- testCases(19) = class'TEST_LogMessage'
+ testCases(3) = class'TEST_SignalsSlots'
+ testCases(4) = class'TEST_UnrealAPI'
+ testCases(5) = class'TEST_Aliases'
+ testCases(6) = class'TEST_ColorAPI'
+ testCases(7) = class'TEST_Text'
+ testCases(8) = class'TEST_TextAPI'
+ testCases(9) = class'TEST_Parser'
+ testCases(10) = class'TEST_JSON'
+ testCases(11) = class'TEST_TextCache'
+ testCases(12) = class'TEST_User'
+ testCases(13) = class'TEST_Memory'
+ testCases(14) = class'TEST_DynamicArray'
+ testCases(15) = class'TEST_AssociativeArray'
+ testCases(16) = class'TEST_CollectionsMixed'
+ testCases(17) = class'TEST_Iterator'
+ testCases(18) = class'TEST_Command'
+ testCases(19) = class'TEST_CommandDataBuilder'
+ testCases(20) = class'TEST_LogMessage'
}
\ No newline at end of file