From c6a0565314432bf4e952fe77153ee5e61d8075a8 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 17 Apr 2021 17:43:47 +0700 Subject: [PATCH] Add `ActorBox` / `ActorRef` / `NativeActorRef` --- sources/Manifest.uc | 42 ++-- sources/Types/ActorService.uc | 294 +++++++++++++++++++++++ sources/Types/Boxes/Native/ActorBox.uc | 105 ++++++++ sources/Types/Refs/Native/ActorRef.uc | 95 ++++++++ sources/Types/Tests/MockAcediaActor.uc | 7 + sources/Types/Tests/MockNativeActor.uc | 7 + sources/Types/Tests/TEST_ActorService.uc | 160 ++++++++++++ sources/Unreal/NativeActorRef.uc | 95 ++++++++ sources/Unreal/UnrealAPI.uc | 14 ++ 9 files changed, 799 insertions(+), 20 deletions(-) create mode 100644 sources/Types/ActorService.uc create mode 100644 sources/Types/Boxes/Native/ActorBox.uc create mode 100644 sources/Types/Refs/Native/ActorRef.uc create mode 100644 sources/Types/Tests/MockAcediaActor.uc create mode 100644 sources/Types/Tests/MockNativeActor.uc create mode 100644 sources/Types/Tests/TEST_ActorService.uc create mode 100644 sources/Unreal/NativeActorRef.uc 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