diff --git a/sources/Manifest.uc b/sources/Manifest.uc
index 7b60c6c..789e0f4 100644
--- a/sources/Manifest.uc
+++ b/sources/Manifest.uc
@@ -26,30 +26,32 @@ defaultproperties
commands(0) = class'ACommandHelp'
commands(1) = class'ACommandDosh'
commands(2) = class'ACommandNick'
+ commands(3) = class'ACommandTest'
services(0) = class'ConnectionService'
services(1) = class'PlayerService'
aliasSources(0) = class'AliasSource'
aliasSources(1) = class'WeaponAliasSource'
aliasSources(2) = class'ColorAliasSource'
testCases(0) = class'TEST_Base'
- testCases(1) = class'TEST_Boxes'
- testCases(2) = class'TEST_Refs'
- 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'
+ testCases(1) = class'TEST_ActorService'
+ testCases(2) = class'TEST_Boxes'
+ testCases(3) = class'TEST_Refs'
+ testCases(4) = class'TEST_SignalsSlots'
+ testCases(5) = class'TEST_UnrealAPI'
+ testCases(6) = class'TEST_Aliases'
+ testCases(7) = class'TEST_ColorAPI'
+ testCases(8) = class'TEST_Text'
+ testCases(9) = class'TEST_TextAPI'
+ testCases(10) = class'TEST_Parser'
+ testCases(11) = class'TEST_JSON'
+ testCases(12) = class'TEST_TextCache'
+ testCases(13) = class'TEST_User'
+ testCases(14) = class'TEST_Memory'
+ testCases(15) = class'TEST_DynamicArray'
+ testCases(16) = class'TEST_AssociativeArray'
+ testCases(17) = class'TEST_CollectionsMixed'
+ testCases(18) = class'TEST_Iterator'
+ testCases(19) = class'TEST_Command'
+ testCases(20) = class'TEST_CommandDataBuilder'
+ testCases(21) = class'TEST_LogMessage'
}
\ No newline at end of file
diff --git a/sources/Types/ActorService.uc b/sources/Types/ActorService.uc
new file mode 100644
index 0000000..46213bd
--- /dev/null
+++ b/sources/Types/ActorService.uc
@@ -0,0 +1,294 @@
+/**
+ * Service for safe `Actor` storage, aimed to resolve two issues that arise
+ * from storing references to `Actor`s inside non-`Actor` objects:
+ * 1. This can potentially prevent the whole level from being
+ * garbage-collected on level change, leading to excessive memory use
+ * and crashes;
+ * 2. This can lead to stored `Actor` temporarily being put into
+ * a "faulty state", where any sort of access to that `Actor`
+ * (except converting it into a `string` ¯\_(ツ)_/¯) will lead to
+ * server/game crashing.
+ * This `Service` resolves these issues by storing `Actor`s in such a way
+ * that they can be relatively quickly obtained via a simple struct value
+ * (`ActorReference`) that can be safely stored inside any `Object`.
+ * It is recommended to use `ActorRef` / `ActorBox` instead of accessing
+ * this `Service` directly.
+ * 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 ActorService extends Service;
+
+/**
+ * This service works as follows: it contains an array filled with `Actor`s
+ * that will dynamically grow in size whenever more space is needed for new
+ * `Actor`s. To refer to the stored `Actor` we put it's index in the storage
+ * array in `ActorReference` struct.
+ *
+ * The main problem with that approach is that once `Actor` is destroyed or
+ * removed- it leaves a "hole" inside our storage and if we keep simply adding
+ * new `Actor`s at the end of the storage array - we will eventually use
+ * too much space, even if we only store a limited amount of `Actor`s at any
+ * given point in time.
+ * To avoid this problem we also maintain an array of "empty indices" that
+ * remembers where the "holes" are located and can let us re-allocate them to
+ * place new `Actor`s in, without needlessly increasing storage's size.
+ *
+ * Another problem is that now two different `Actor`s can occupy the same
+ * spot at different points in time. To deal with that we will also record
+ * an `Actor`'s "version" for each storage index and increment it each time
+ * a new `Actor` is stored at that index. We will also store that "version"
+ * inside `ActorReference` to help us invalidate references to old `Actor`s
+ * that were stored there before.
+ *
+ * NOTE that this `Service` avoids doing any sort of manual cleaning,
+ * instead relying on the proper interface (`ActorRef` / `ActorBox`) to do it.
+ */
+
+/**
+ * Reference to the `Actor` storage inside this service.
+ * Reference is only considered invalid after an `Actor` it was referring
+ * to was removed from the storage (possibly by being replaced with
+ * another `Actor`).
+ */
+struct ActorReference
+{
+ var private int index;
+ var private int version;
+};
+
+/* Arrays `actorRecords`, `actorVersions` and `indexAllocationFlag` could
+ * be represented as an array of a single struct with 3 element, but instead,
+ * for performance reasons, were separated into three distinct arrays - one for
+ * each variable.
+ * This means that they must at all times have the same length and
+ * elements with the same index should be considered to belong to
+ * the same record.
+ */
+// Storage for `Actor`s. It's size can grow, but it cannot shrink, instead
+// marking some of array's indices as vacant
+// (by putting them into `emptyIndices`).
+var private array actorRecords;
+// Defines a "version" of a corresponding `Actor`.
+var private array actorVersions;
+// Marks whether values at a particular index are currently used to
+// store some `Actor` (`1`) or not (`0`).
+// Without these flags we would have no way of knowing whether a spot in
+// the array was already freed and recorded in `emptyIndices`, since stored
+// `Actor`s can turn into `none` by themselves upon destruction.
+var private array indexAllocationFlag;
+
+// A set of empty indices - that once contained stored an `Actor`, but can
+// now be reused.
+// Used like a LIFO-queue to quickly find an empty spot in `actorRecords`.
+var private array emptyIndices;
+
+// Finds an vacant index in our records (expands array if all indices are
+// already taken) and puts `candidate` there.
+// Does not do any checks for whether `candidate != none`.
+private final function int InsertAtEmptyIndex(Actor candidate)
+{
+ local int newIndex;
+ if (emptyIndices.length > 0)
+ {
+ newIndex = emptyIndices[emptyIndices.length - 1];
+ emptyIndices.length = emptyIndices.length - 1;
+ actorVersions[newIndex] += 1;
+ }
+ else
+ {
+ newIndex = actorRecords.length;
+ actorVersions[newIndex] = 0;
+ }
+ actorRecords[newIndex] = candidate;
+ indexAllocationFlag[newIndex] = 1;
+ return newIndex;
+}
+
+// Forces `indexToFree` to become vacant, not storing any `Actor`.
+private final function FreeIndex(int indexToFree)
+{
+ if (indexAllocationFlag[indexToFree] > 0)
+ {
+ actorRecords[indexToFree] = none;
+ emptyIndices[emptyIndices.length] = indexToFree;
+ }
+ // No need to bump `actorVersions[indexToFree]` here, since any refences to
+ // this index will automatically be invalidated by this:
+ indexAllocationFlag[indexToFree] = 0;
+}
+
+/**
+ * Adds a new `Actor` to the storage.
+ *
+ * Any reference created with `AddActor()` must be release with
+ * `RemoveActor()`.
+ *
+ * This method does not attempt to check whether `newActor` is already stored
+ * in `ActorService`. Therefore it is possible for the same actor to be stored
+ * multiple times under different `ActorReference` records.
+ *
+ * Can only fail if provided `Actor` is either equal to `none`.
+ *
+ * Note that while this storage is supposed to be "safe", meaning that it
+ * aims to reduce probability of `Actor`-related crashes, it assumes that
+ * passed `Actor`s are not already "broken", as there is no way to check
+ * for that.
+ * `Actor`s that were not stored in a non-`Actor` object as a non-local
+ * variable should not be broken.
+ *
+ * @param newActor `Actor` to store. If it's equal to `none`, then
+ * this method will do nothing.
+ * @return `ActorReference` struct that can be used to retrieve stored
+ * `Actor` later. Returned reference will point at `none` once stored
+ * `Actor` gets destroyed or removed via the `RemoveActor()` call.
+ */
+public final function ActorReference AddActor(Actor newActor)
+{
+ local int newIndex;
+ local ActorReference result;
+ // This will make the very first check inside `ValidateIndexVersion()` fail
+ result.index = -1;
+ if (newActor == none) {
+ return result;
+ }
+
+ newIndex = InsertAtEmptyIndex(newActor);
+ result.index = newIndex;
+ result.version = actorVersions[newIndex];
+ return result;
+}
+
+/**
+ * Returns an `Actor` that provided `reference` points at.
+ *
+ * @param reference Reference to the `Actor` this method should return.
+ * @return `Actor`, stored in caller `ActorService` with `reference`.
+ * If stored `Actor` was already destroyed or removed, this method will
+ * return `none`.
+ */
+public final function Actor GetActor(ActorReference reference)
+{
+ local int index;
+ local Actor result;
+ index = reference.index;
+ if (!ValidateIndexVersion(index, reference.version)) {
+ return none;
+ }
+ // While this can be considered a side-effect in a getter, removing `none`
+ // from the storage will not affect how `reference` behaves, since it can
+ // only refer to `none` from now on.
+ result = actorRecords[index];
+ if (result == none)
+ {
+ FreeIndex(index);
+ return none;
+ }
+ return result;
+}
+
+/**
+ * Replaces an `Actor`, that provided `reference` points at, with a `newActor`,
+ * returning a new `ActorReference` and invalidating the old one (`reference`),
+ * making it to refer to `none`.
+ *
+ * This method is guaranteed to be functionally identical (as far as public,
+ * not encapsulated, interface is concerned) to calling two methods:
+ * `RemoveActor(reference)` and `AddActor(newActor)`. But is expected to
+ * provide be more efficient.
+ *
+ * @param reference Reference to `Actor` that should be replaced.
+ * @param newActor New `Actor` to store.
+ * @return Reference to the `newActor` inside the storage.
+ */
+public final function ActorReference UpdateActor(
+ ActorReference reference,
+ Actor newActor)
+{
+ local int index;
+ index = reference.index;
+ // Nothing to remove
+ if (!ValidateIndexVersion(index, reference.version)) {
+ return AddActor(newActor);
+ }
+ // Nothing to add
+ if (newActor == none) {
+ RemoveActor(reference);
+ }
+ else
+ {
+ // If we need to both remove and add, we can just replace an `Actor`
+ // and bump the stored version to invalidate copies of the passed
+ // `reference`:
+ actorRecords[index] = newActor;
+ actorVersions[index] += 1;
+ reference.version += 1;
+ }
+ return reference;
+}
+
+/**
+ * Unconditionally removes an `Actor` recorded with `reference` from storage.
+ *
+ * Cannot fail.
+ *
+ * If referred `Actor` is recorded in storage under several different
+ * references - only this reference will be affected. The rest are still going
+ * to refer to that `Actor`.
+ *
+ * @param reference Reference to `Actor` to be removed.
+ */
+public final function RemoveActor(ActorReference reference)
+{
+ if (ValidateIndexVersion(reference.index, reference.version)) {
+ FreeIndex(reference.index);
+ }
+}
+
+/**
+ * Returns size of this storage.
+ *
+ * @param totalSize Default value (`false`) means that method should return
+ * amount of currently stored `Actor`s, while `true` means how much space
+ * this storage takes up (in the sense that the actual amount in bytes is
+ * `O(GetSize(true))` in big-O notation).
+ * @return Size of this storage.
+ */
+public final function int GetSize(optional bool totalSize)
+{
+ if (totalSize) {
+ return actorRecords.length;
+ }
+ return actorRecords.length - emptyIndices.length;
+}
+
+// Validates passed index and `Actor`'s version, taken from
+// the `ActorReference`: returns `true` iff `Actor` referred to by them
+// currently exists in this storage (even if it's now equal to `none`).
+private final function bool ValidateIndexVersion(
+ int refIndex,
+ int refVersion)
+{
+ if (refIndex < 0) return false;
+ if (refIndex >= actorRecords.length) return false;
+ if (actorVersions[refIndex] != refVersion) return false;
+
+ return (indexAllocationFlag[refIndex] > 0);
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Types/Boxes/Native/ActorBox.uc b/sources/Types/Boxes/Native/ActorBox.uc
new file mode 100644
index 0000000..6d8ff6a
--- /dev/null
+++ b/sources/Types/Boxes/Native/ActorBox.uc
@@ -0,0 +1,105 @@
+/**
+ * This file either was manually edited with minimal changes from the template
+ * for value boxes.
+ * 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 ActorBox extends ValueBox
+ dependson(ActorService);
+
+var protected int boxHashCode;
+var protected bool hasValue;
+var protected ActorService.ActorReference valueRef;
+
+protected function Finalizer()
+{
+ local ActorService service;
+ if (hasValue) {
+ service = ActorService(class'ActorService'.static.Require());
+ }
+ if (service != none) {
+ service.RemoveActor(valueRef);
+ }
+ hasValue = false;
+}
+
+/**
+ * Returns stored value.
+ *
+ * @return Value, stored in this reference.
+ */
+public final function AcediaActor Get()
+{
+ local ActorService service;
+ if (!hasValue) {
+ return none;
+ }
+ service = ActorService(class'ActorService'.static.Require());
+ if (service != none) {
+ return AcediaActor(service.GetActor(valueRef));
+ }
+ return none;
+}
+
+/**
+ * Initialized box value. Can only be called once.
+ *
+ * @param boxValue Value to store in this reference.
+ * @return Reference to the caller `ActorBox` to allow for method chaining.
+ */
+public final function ActorBox Initialize(AcediaActor boxValue)
+{
+ local ActorService service;
+ if (IsInitialized()) {
+ return self;
+ }
+ service = ActorService(class'ActorService'.static.Require());
+ if (service == none) {
+ return self;
+ }
+ valueRef = service.AddActor(boxValue);
+ hasValue = true;
+ MarkInitialized();
+ if (boxValue != none) {
+ boxHashCode = boxValue.GetHashCode();
+ }
+ else {
+ boxHashCode = super.GetHashCode();
+ }
+ return self;
+}
+
+public function bool IsEqual(Object other)
+{
+ local ActorBox otherBox;
+ local ActorService service;
+ otherBox = ActorBox(other);
+ if (otherBox == none) return false;
+ service = ActorService(class'ActorService'.static.Require());
+ if (service == none) return false;
+
+ return Get() == otherBox.Get();
+}
+
+protected function int CalculateHashCode()
+{
+ return boxHashCode;
+}
+
+defaultproperties
+{
+}
diff --git a/sources/Types/Refs/Native/ActorRef.uc b/sources/Types/Refs/Native/ActorRef.uc
new file mode 100644
index 0000000..3ef44fc
--- /dev/null
+++ b/sources/Types/Refs/Native/ActorRef.uc
@@ -0,0 +1,95 @@
+/**
+ * This file either was manually edited with minimal changes from the template
+ * for value references.
+ * 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 ActorRef extends ValueRef
+ dependson(ActorService);
+
+var protected bool hasValue;
+var protected ActorService.ActorReference valueRef;
+
+protected function Finalizer()
+{
+ local ActorService service;
+ if (hasValue) {
+ service = ActorService(class'ActorService'.static.Require());
+ }
+ if (service != none) {
+ service.RemoveActor(valueRef);
+ }
+ hasValue = false;
+}
+
+/**
+ * Returns stored value.
+ *
+ * @return Value, stored in this reference.
+ */
+public final function AcediaActor Get()
+{
+ local ActorService service;
+ if (!hasValue) {
+ return none;
+ }
+ service = ActorService(class'ActorService'.static.Require());
+ if (service != none) {
+ return AcediaActor(service.GetActor(valueRef));
+ }
+ return none;
+}
+
+/**
+ * Changes stored value. Cannot fail.
+ *
+ * @param newValue New value to store in this reference.
+ * @return Reference to the caller `ActorRef` to allow for
+ * method chaining.
+ */
+public final function ActorRef Set(AcediaActor newValue)
+{
+ local ActorService service;
+ service = ActorService(class'ActorService'.static.Require());
+ if (service == none) {
+ return none;
+ }
+ if (hasValue) {
+ valueRef = service.UpdateActor(valueRef, newValue);
+ }
+ else {
+ valueRef = service.AddActor(newValue);
+ }
+ hasValue = true;
+ return self;
+}
+
+public function bool IsEqual(Object other)
+{
+ local ActorRef otherBox;
+ local ActorService service;
+ otherBox = ActorRef(other);
+ if (otherBox == none) return false;
+ service = ActorService(class'ActorService'.static.Require());
+ if (service == none) return false;
+
+ return Get() == otherBox.Get();
+}
+
+defaultproperties
+{
+}
diff --git a/sources/Types/Tests/MockAcediaActor.uc b/sources/Types/Tests/MockAcediaActor.uc
new file mode 100644
index 0000000..4c7d653
--- /dev/null
+++ b/sources/Types/Tests/MockAcediaActor.uc
@@ -0,0 +1,7 @@
+class MockAcediaActor extends AcediaActor;
+
+defaultproperties
+{
+ bCollideActors = false
+ bBlockActors = false
+}
\ No newline at end of file
diff --git a/sources/Types/Tests/MockNativeActor.uc b/sources/Types/Tests/MockNativeActor.uc
new file mode 100644
index 0000000..43ef8da
--- /dev/null
+++ b/sources/Types/Tests/MockNativeActor.uc
@@ -0,0 +1,7 @@
+class MockNativeActor extends Actor;
+
+defaultproperties
+{
+ bCollideActors = false
+ bBlockActors = false
+}
\ No newline at end of file
diff --git a/sources/Types/Tests/TEST_ActorService.uc b/sources/Types/Tests/TEST_ActorService.uc
new file mode 100644
index 0000000..d2d94ab
--- /dev/null
+++ b/sources/Types/Tests/TEST_ActorService.uc
@@ -0,0 +1,160 @@
+/**
+ * Set of tests for `ActorService`'s functionality.
+ * 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_ActorService extends TestCase
+ dependson(ActorService)
+ abstract;
+
+protected static function TESTS()
+{
+ local ActorService service;
+ Context("Testing adding and retrieving native `Actor`s from"
+ @ "`ActorService`.");
+ Issue("Cannot get instance of `ActorService`.");
+ service = ActorService(class'ActorService'.static.Require());
+ TEST_ExpectNotNone(service);
+ Test_AddDestroy(class'MockNativeActor');
+ Test_AddDestroy(class'MockAcediaActor');
+ Test_AddRemove(class'MockNativeActor');
+ Test_AddRemove(class'MockAcediaActor');
+ Test_Update(class'MockNativeActor');
+ Test_Update(class'MockAcediaActor');
+}
+
+protected static function Test_AddDestroy(class classToTest)
+{
+ local int i;
+ local ActorService service;
+ local array actorRefs;
+ local array nativeActors;
+ service = ActorService(class'ActorService'.static.Require());
+ Issue("Cannot retrieve recorded `Actor`s with `GetActor()`.");
+ for (i = 0; i < 1000; i += 1)
+ {
+ nativeActors[i] = Actor(__().memory.Allocate(classToTest));
+ actorRefs[i] = service.AddActor(nativeActors[i]);
+ }
+ for (i = 0; i < 1000; i += 1)
+ {
+ TEST_ExpectNotNone(service.GetActor(actorRefs[i]));
+ TEST_ExpectTrue(service.GetActor(actorRefs[i]) == nativeActors[i]);
+ }
+ Issue("Just destroyed `Actor`s are not returned as `none` from"
+ @ "`GetActor()`.");
+ for (i = 0; i < 1000; i += 1)
+ {
+ if (i % 2 == 1) {
+ continue;
+ }
+ nativeActors[i].Destroy();
+ TEST_ExpectNone(service.GetActor(actorRefs[i]));
+ }
+
+ Issue("`Actor`s are not properly added to service after destruction of the"
+ @ "previously stored ones.");
+ for (i = 0; i < 1000; i += 1)
+ {
+ if (i % 2 == 0)
+ {
+ nativeActors[i] = Actor(__().memory.Allocate(classToTest));
+ actorRefs[i] = service.AddActor(nativeActors[i]);
+ }
+ }
+ for (i = 0; i < 1000; i += 1)
+ {
+ TEST_ExpectNotNone(service.GetActor(actorRefs[i]));
+ TEST_ExpectTrue(service.GetActor(actorRefs[i]) == nativeActors[i]);
+ }
+}
+
+protected static function Test_AddRemove(class classToTest)
+{
+ local int i;
+ local ActorService service;
+ local array actorRefs;
+ local array nativeActors;
+ service = ActorService(class'ActorService'.static.Require());
+ for (i = 0; i < 1000; i += 1)
+ {
+ nativeActors[i] = Actor(__().memory.Allocate(classToTest));
+ actorRefs[i] = service.AddActor(nativeActors[i]);
+ }
+
+ Issue("Just `Remove()`-ed `Actor`s are not returned as `none` from"
+ @ "`GetActor()`.");
+ for (i = 0; i < 1000; i += 1)
+ {
+ if (i % 2 == 1) {
+ continue;
+ }
+ service.RemoveActor(actorRefs[i]);
+ TEST_ExpectNone(service.GetActor(actorRefs[i]));
+ }
+
+ Issue("`Actor`s are not properly added to service after removal of the"
+ @ "previously stored ones.");
+ for (i = 0; i < 1000; i += 1)
+ {
+ if (i % 2 == 0)
+ {
+ nativeActors[i] = Actor(__().memory.Allocate(classToTest));
+ actorRefs[i] = service.AddActor(nativeActors[i]);
+ }
+ }
+ for (i = 0; i < 1000; i += 1)
+ {
+ TEST_ExpectNotNone(service.GetActor(actorRefs[i]));
+ TEST_ExpectTrue(service.GetActor(actorRefs[i]) == nativeActors[i]);
+ }
+}
+
+protected static function Test_Update(class classToTest)
+{
+ local int i;
+ local ActorService service;
+ local array actorRefs;
+ local array nativeActors;
+ service = ActorService(class'ActorService'.static.Require());
+ for (i = 0; i < 1000; i += 1)
+ {
+ nativeActors[i] = Actor(__().memory.Allocate(classToTest));
+ actorRefs[i] = service.AddActor(nativeActors[i]);
+ }
+
+ Issue("`Actor`s are not properly updated.");
+ for (i = 0; i < 1000; i += 1)
+ {
+ if (i % 2 == 0)
+ {
+ nativeActors[i] = Actor(__().memory.Allocate(classToTest));
+ actorRefs[i] = service.UpdateActor(actorRefs[i], nativeActors[i]);
+ }
+ }
+ for (i = 0; i < 1000; i += 1)
+ {
+ TEST_ExpectNotNone(service.GetActor(actorRefs[i]));
+ TEST_ExpectTrue(service.GetActor(actorRefs[i]) == nativeActors[i]);
+ }
+}
+
+defaultproperties
+{
+ caseGroup = "Types"
+ caseName = "ActorService"
+}
\ No newline at end of file
diff --git a/sources/Unreal/NativeActorRef.uc b/sources/Unreal/NativeActorRef.uc
new file mode 100644
index 0000000..65bd227
--- /dev/null
+++ b/sources/Unreal/NativeActorRef.uc
@@ -0,0 +1,95 @@
+/**
+ * This file either was manually edited with minimal changes from the template
+ * for value references.
+ * 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 NativeActorRef extends ValueRef
+ dependson(ActorService);
+
+var protected bool hasValue;
+var protected ActorService.ActorReference valueRef;
+
+protected function Finalizer()
+{
+ local ActorService service;
+ if (hasValue) {
+ service = ActorService(class'ActorService'.static.Require());
+ }
+ if (service != none) {
+ service.RemoveActor(valueRef);
+ }
+ hasValue = false;
+}
+
+/**
+ * Returns stored value.
+ *
+ * @return Value, stored in this reference.
+ */
+public final function Actor Get()
+{
+ local ActorService service;
+ if (!hasValue) {
+ return none;
+ }
+ service = ActorService(class'ActorService'.static.Require());
+ if (service != none) {
+ return service.GetActor(valueRef);
+ }
+ return none;
+}
+
+/**
+ * Changes stored value. Cannot fail.
+ *
+ * @param newValue New value to store in this reference.
+ * @return Reference to the caller `NativeActorRef` to allow for
+ * method chaining.
+ */
+public final function NativeActorRef Set(Actor newValue)
+{
+ local ActorService service;
+ service = ActorService(class'ActorService'.static.Require());
+ if (service == none) {
+ return none;
+ }
+ if (hasValue) {
+ valueRef = service.UpdateActor(valueRef, newValue);
+ }
+ else {
+ valueRef = service.AddActor(newValue);
+ }
+ hasValue = true;
+ return self;
+}
+
+public function bool IsEqual(Object other)
+{
+ local NativeActorRef otherBox;
+ local ActorService service;
+ otherBox = NativeActorRef(other);
+ if (otherBox == none) return false;
+ service = ActorService(class'ActorService'.static.Require());
+ if (service == none) return false;
+
+ return Get() == otherBox.Get();
+}
+
+defaultproperties
+{
+}
diff --git a/sources/Unreal/UnrealAPI.uc b/sources/Unreal/UnrealAPI.uc
index 63f75dc..c487216 100644
--- a/sources/Unreal/UnrealAPI.uc
+++ b/sources/Unreal/UnrealAPI.uc
@@ -241,6 +241,20 @@ public final function array GetAllInventoryFrom(
return result;
}
+/**
+ * Creates reference object to store a `Actor` value.
+ *
+ * @param value Initial value to store in reference.
+ * @return `NativeActorRef`, containing `value`.
+ */
+public final function NativeActorRef ActorRef(optional Actor value)
+{
+ local NativeActorRef ref;
+ ref = NativeActorRef(_.memory.Allocate(class'NativeActorRef'));
+ ref.Set(value);
+ return ref;
+}
+
defaultproperties
{
}
\ No newline at end of file