UnrealScript library and basis for all Acedia Framework mods
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

589 lines
23 KiB

3 years ago
# `AcediaObject` and `AcediaActor`
Acedia defines its own base classes for both actor (`AcediaActor`)
and non-actor objects (`AcediaObject`), better integrated into
Acedia's infrastructure.
Here we will go over everything you need to understand them.`Object` and `Actor`.
## Who is responsible for objects?
If you've already read [safety rules](./safety.md) (and you should have),
then you already know about the importance of deallocation.
But which objects exactly are you supposed to deallocate?
Understanding what objects you are responsible for is likely the most important
concept to get when working with Acedia.
There are two main guidelines:
* **If function returns an object (as a return value or as an `out` argument) -
then this object must be deallocated by
whoever called that function.**
If you've called `_.text.Empty()`, then you must deallocate
the `MutableText`object it returned.
Conversely, if you are implementing function that returns an object,
then you must not deallocate it yourself.
In fact, you are expected not to use that object at all,
since now you can't know when it will be deallocated.
* **Functions do not deallocate their arguments.**
If you pass an object as an argument to a function - you can expect
that it won't be deallocated during that call.
It might get *modified*, but not *deallocated*.
And, again, when implementing your own function - you should not deallocate
its arguments either.
However, these guidelines should be treated as *default assumptions* and
not *hard rules*.
### Exceptions
First guideline, for example, can be broken if returned object is supposed to
be shared: `_.players.GetPlayers()` returns array with references to
*player objects* (`array<APLayer>`) that aren't supposed to ever be deallocated.
Similarly, Acedia's collections operate by different rules:
they might still consider themselves responsible for objects returned with
`GetItem()`.
Second guideline can also be broken by some of the methods for the sake of
convenience.
If you need to turn a `Text` object `textToConvert` into a `string`,
then you can either do:
```unrealscript
if (textToConvert != none)
{
result = textToConvert.ToPlainString();
textToConvert.FreeSelf();
}
```
or simply call `_.text.ToString()` that automatically deallocates its argument:
`result = _.text.ToString(textToConvert)`.
> Any such exceptions are documented (or at least should be), so simply read
> the docs for functions you're using.
> If they don't mention anything about how their arguments or return values
> should be treated - assume above stated guidelines.
## `MemoryAPI`
The majority, if not all, of the Acedia's objects you will be using are going to
be created by specialized methods like `_.text.FromString()`,
`_.collections.EmptyDynamicArray()` or `_.time.StartTimer()`
and can be deallocated with `self.FreeSelf()` method.
However, that won't be enough if you want to create and allocate your own
classes, for that you'll need the help of `MemoryAPI`.
They are less powerful than `new` keyword and `Spawn()` function, but perform
certain background work, necessary for Acedia to function and
**you should always use them for creating Acedia's objects**.
Ultimately, all Acedia's objects and actors are created with
`_.memory.Allocate()` and "destroyed" with `_.memory.Free()`.
For example, here is how new `Parser` is created with `_.text.NewParser()`:
```unrealscript
public final function Parser NewParser()
{
return Parser(_.memory.Allocate(class'Parser'));
}
```
and `self.FreeSelf()` is actually defined in `AcediaObject`
and `AcediaActor` as follows (ignore parts about life versions for now,
they will be explained in sections below):
```unrealscript
public final function FreeSelf(optional int lifeVersion)
{
if (lifeVersion <= 0 || lifeVersion == GetLifeVersion()) {
_.memory.Free(self);
}
}
```
These two functions are the most important ones in `MemoryAPI`,
but it contains several more useful ones:
| Function | Description |
| -------- | ----------- |
| `Allocate(class<Object>, optional bool)` | Creates a new `Object` / `Actor` of a given class. `bool` argument allows to forbid reallocation, forcing creation of a new object.
| `LoadClass(Text)` | Creates a class instance from its textual representation. |
| `AllocateByReference(Text, optional bool)` | Same as `Allocate()`, but takes textual representation of the class as an argument. |
| `Free(Object)` | Deallocates provided object. |
| `FreeMany(array<Object>)` | Deallocates every object inside given array. |
| `CollectGarbage(optional bool)` | Forces garbage collection. By default also includes all deallocated (but not destroyed) objects. `bool` argument allows to skip collecting them.
> **NOTE:** `MemoryAPI` can also be used for creating objects that do not
> derive from either `AcediaObject` or `AcediaActor`, but there is no point in
> using them over `new` or `Spawn()`:
> Acedia will not reallocate non-Acedia objects.
## Constructors and finalizers
Both `AcediaObject` and `AcediaActor` support
[constructors](
https://en.wikipedia.org/wiki/Constructor_(object-oriented_programming))
and
[finalizers](https://en.wikipedia.org/wiki/Finalizer).
*Constructor* is a method that's called on object after it's created,
preparing it for use.
In Acedia *Finalizer* is a method that's called when object is deallocated
(or actor is destroyed) and can be used to clean up any used resources.
> Technically, right now *destructor* might be a better terminology for Acedia's
> finalizers, but, if development is not halted, current name would eventually
> become a better fit.
A good and simple example is from the `ATradingComponent` that
allocates necessary objects inside its constructor and deallocates them in
its finalizer:
```unrealscript
protected function Constructor()
{
onStartSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
onEndSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
onTraderSelectSignal = Trading_OnSelect_Signal(
_.memory.Allocate(class'Trading_OnSelect_Signal'));
}
protected function Finalizer()
{
_.memory.Free(onStartSignal);
_.memory.Free(onEndSignal);
_.memory.Free(onTraderSelectSignal);
onStartSignal = none;
onEndSignal = none;
onTraderSelectSignal = none;
}
```
To use constructors and finalizers in your own classes you simply need to
overload `Constructor()` and `Finalizer()` methods (they are defined in both
`AcediaObject` and `AcediaActor`), just like in the example above.
> Acedia's constructors do not take parameters and because of that some classes
> also define `Initialize()` method that is required to be used right after
> an object was allocated.
## Object equality and object hash
Comparing object variable with `==` operator simply checks if they refer to
the exact same object.
But sometimes we want a comparison that compares the content of two objects
instead: like checking that two different `Text`s store the exact same data.
Acedia provides an alternative way to compare two objects - `IsEqual()` method.
Its default implementation corresponds to that of `==` operator:
```unrealscript
public function bool IsEqual(Object other)
{
return (self == other);
}
```
but can be redefined, as long as it obeys following rules:
* `a.IsEqual(a) == true`;
* `a.IsEqual(b)` if and only if `b.IsEqual(a)`;
* Result of `a.IsEqual(b)` does not change unless one of the objects gets
deallocated.
Because of last rule two `MutableText`s cannot be compared base on their content
since their contents can change without deallocation.
Reimplementing `IsEqual()` method also requires you to reimplement how object's
[hash value](https://en.wikipedia.org/wiki/Hash_function) is calculated.
*Hash value* is a an `int` associated with an object.
Several different objects can have the same hash value and equal objects *must*
have the same hash value.
By default, Acedia's objects simply use randomly generated value as their hash.
This can be changed by reimplementing `CalculateHashCode()` method.
Every object will only call it once to cache it for `GetHashCode()`:
```unrealscript
public final function int GetHashCode()
{
if (_hashCodeWasCached) {
return _cachedHashCode;
}
_hashCodeWasCached = true;
_cachedHashCode = CalculateHashCode();
return _cachedHashCode;
}
```
As an example, here is `Text`'s definition that calculates hash based on
the contents:
```unrealscript
protected function int CalculateHashCode()
{
local int i;
local int hash;
hash = 5381;
for (i = 0; i < codePoints.length; i += 1)
{
// hash * 33 + codePoints[i]
hash = ((hash << 5) + hash) + codePoints[i];
}
return hash;
}
```
## Boxing
Last important topic to go over is
[boxing](
https://en.wikipedia.org/wiki/Object_type_(object-oriented_programming)#Boxing),
a process of turning primitive types such as `bool`, `byte`, `int` or `float`
into objects.
The concept is very simple - we create a *box* object, which is just an object
that stores a single primitive value and could be implemented kind of like that:
```unrealscript
class MyBox extends Object;
var float value;
```
Except Acedia's boxes are *immutable* - their value cannot change once
the box was created.
Boxes were introduced because they allowed creation of general collections:
Acedia's collections can only store `AcediaObject`, but thanks to boxing
any value can be turned into `AcediaObject` and stored in the collection.
For native primitive types they can be created with either `BoxAPI` or manually:
```unrealscript
local IntBox box1;
local FloatBox box2;
// Created with `BoxAPI`
box1 = _.box.int(7);
// Allocated and initialized manually
box2 = FloatBox(_.memory.Allocate(class'FloatBox'));
box2.Initialize(-2.48); // Must be done immediately after allocation!
// Works the same
Log("Int value:" @ box1.Get()); // Int value: 7
Log("Float value:" @ box2.Get()); // Float value: -2.48
```
Immutable boxes also have a counterpart - mutable *references* that also
provide `Set()` method:
```unrealscript
local IntRef ref1;
local FloatRef ref2;
// Created with `BoxAPI`
ref1 = _.ref.int(7);
// Allocated and initialized manually
ref2 = FloatRef(_.memory.Allocate(class'FloatRef'));
ref2.Initialize(-2.48); // Must be done immediately after allocation!
// Change values
ref1.Set(-89);
ref2.Set(0.56);
Log("Int value:" @ ref1.Get()); // Int value: -89
Log("Float value:" @ ref2.Get()); // Float value: 0.56
```
The most important difference between boxes and references concerns how their
`IsEqual()` and `GetHash()` are implemented:
* Since boxes redefine `IsEqual()` and `GetHash()` to depend on the stored value.
Since value inside the box cannot change, then there is no problem to base
equality and hash on it.
* References do not redefine `IsEqual()` / `GetHash()` and behave like any
other object - their hash is random and they are only equal to themselves.
```unrealscript
local ByteBox box1, box2;
local ByteRef ref1, ref2;
box1 = _.box.byte(56);
box2 = _.box.byte(56);
ref1 = _.ref.byte(247);
ref2 = _.ref.byte(247);
// Boxes equality: true
Log("Boxes equality:" @ (box1.IsEqual(box2)));
// Boxes hash equality: true
Log("Boxes hash equality:" @ (box1.GetHash() == box2.GetHash()));
// Refs equality: false
Log("Refs equality:" @ (ref1.IsEqual(ref2)));
// Refs hash equality: false
// (that's the most likely result, but it can actually be `true` by pure chance)
Log("Refs hash equality:" @ (ref1.GetHash() == ref2.GetHash()));
```
> **NOTE:** For `string`s the role of boxes and references is performed by
> `Text` and `MutableText` classes that are discussed separately.
### Actor references with `NativeActorRef`
As was explained in [safety rules](./safety.md), storing references to actors
inside objects is a bad idea.
Actor boxes and references provide us with a safe way to do that:
```unrealscript
class MyObject extends AcediaObject;
var NativeActorRef pawnReference;
// ...
protected function Finalizer()
{
_.memory.Free(pawnReference); // This does not destroy stored pawn!
pawnReference = none;
}
function Pawn GetMyPawn()
{
if (pawnReference == none) {
return none;
}
return Pawn(pawnReference.Get());
}
function SetMyPawn(Pawn newPawn)
{
if (pawnReference == none)
{
// `UnrealAPI` deals with storing non-Acedia actors such as `Pawn`.
// For `AcediaActor`s you can also use `_.ref.Actor()`.
pawnReference = _.unreal.ActorRef(newPawn);
}
else {
pawnReference.Set(newPawn);
}
}
function DoWork()
{
local Pawn myPawn;
myPawn = GetMyPawn();
if (myPawn == none) {
return;
}
// <Some code that might `Destroy()` our pawn>
// ^ After destroying a pawn,
// `myPawn` local variable might go "bad" and cause crashes,
// so it's a good idea to update it from safe `pawnReference`:
myPawn = GetMyPawn();
myPawn.health += 10;
}
```
Actor boxes do not exist, since we cannot guarantee that value stored inside
them will never change - destroying stored actor will always reset it to `none`.
### Array boxes and references
If necessary, box and reference classes can be manually created for any type
of value, including `array<...>`s and `struct`s.
Acedia provides such classes for arrays of primitive types out of the box.
They can be useful for passing huge arrays between objects and functions
by reference, without copying their entire data every time.
They also provide quite a few several convenience methods.
Here is a list for `FloatArrayRef` as an example:
| Method | Description |
| ------ | ----------- |
| `Get()` | Returns the whole stored array as `array<float>`. |
| `Set(array<float>)` | Sets the whole array value. |
| `GetItem(int, optional float)` | Returns item at specified index. If index is invalid, returns passed default value. |
| `SetItem(int, float)` |Changes array's value at specified index. |
| `GetLength()` | Returns length of the array. `ref.GetLength()` is faster than `ref.Get().length`, since latter will make a copy of the whole array first |
| `SetLength(int)` | Resizes stored array, doing nothing on negative input. |
| `Empty()` | Empties stored array. |
| `Add(int)` | Increases length of the array by adding specified amount of new elements at the end. |
| `Insert(int index, int count)` | Inserts `count` empty elements into the array at specified position. The indices of the following elements are increased by `count` in order to make room for the new elements. |
| `Remove(int index, int count)` | Removes number elements from the array, starting at `index`. All elements before position and from `index + count` on are not changed, but the element indices change, - they shift to close the gap, created by removed elements. |
| `RemoveIndex(int)` | Removes value at a given index, shifting all the elements that come after one place backwards. |
| `AddItem(float)` | Adds given `float` at the end of the array, expanding it by 1 element. |
| `InsertItem(int, float)` | Inserts given item at index of the array, shifting all the elements starting from `index` one position to the right. |
| `AddArray(array<float>)` / `AddArrayRef(FloatArrayRef)` | Adds given array of items at the end of the array, expanding it by inserted amount. |
| `InsertArray(array<float>)` / `InsertArrayRef(FloatArrayRef)` | Inserts items array at specified index of the array, shifting all the elements starting from `index` by inserted amount to the right. |
| `RemoveItem(float, bool)` | Returns all occurrences of `item` in the caller `float` (optionally only first one). |
| `Find(float)` | Finds first occurrence of specified item in caller `FloatArrayRef` and returns its index. |
| `Replace(float search, float replacement)` | Replaces any occurrence of `search` with `replacement`. |
| `Sort(optional bool descending)` | Sorts array in either ascending or descending order. |
## [Advanced] Static constructors and finalizers
Acedia also supports a notion of static constructors and finalizers.
Static constructor is called for each class only once:
* Whenever first object of such class is created,
before its constructor is called;
* If you want static initialization to be done earlier,
it is allowed to call static constructor manually:
`class'...'.static.StaticConstructor()`.
> **NOTE:** Static constructor being called for your class does not guarantee it
> being called for its parent class. They are considered independently.
Right now relying on static constructors in not advised, but if you are sure
you need them, you can define them like this:
```unrealscript
public static function StaticConstructor()
{
// This condition is necessary, DO NOT remove it, leave it AS IS
if (StaticConstructorGuard()) {
return;
}
// Place your logic here
// ...
}
```
Static finalizers, however, are more important.
They are called during Acedia's shutdown for any class that had its
static constructor invoked (including for any Acedia class that was allocated).
It can be used to "clean up" after yourself.
To have a clean level change it is important that you undo as many changes to
game's objects as you reasonably can.
It is especially important to reset default values, unless their change is
deliberate.
Here is an example used in the base `AcediaObject` class at some point:
```unrealscript
protected static function StaticFinalizer()
{
// Not cleaning object references in `default` values will interfere
// with garbage collection
default._textCache = none;
default._objectPool = none;
// Not cleaning this value will prevent static constructors
// (and a whole bunch of other code) from being called after the map change
default._staticConstructorWasCalled = false;
}
```
## [Advanced] Technical details
### How allocation and deallocation works
UnrealScript lacks any practical way to destroy objects on demand:
the best one can do is remove any references to the object and wait for
garbage collection.
But garbage collection itself is too slow and causes noticeable lag spikes
for players, making it suitable only for cleaning objects when switching levels.
To alleviate this problem, there exists a standard class `ObjectPool`
that stores unused objects inside dynamic array until they are needed.
Unfortunately, using a single `ObjectPool` for a large volume of objects is
impractical from performance perspective, since it stores objects of
all classes together and each object allocation from the pool can potentially
require going through the whole array:
```unrealscript
simulated function Object AllocateObject(class ObjectClass)
{
local Object Result;
local int ObjectIndex;
for(ObjectIndex = 0;ObjectIndex < Objects.Length;ObjectIndex++)
{
if(Objects[ObjectIndex].Class == ObjectClass)
{
Result = Objects[ObjectIndex];
Objects.Remove(ObjectIndex,1);
break;
}
}
if(Result == None)
Result = new(Outer) ObjectClass;
return Result;
}
```
Acedia uses a separate object pool (implemented by `AcediaObjectPool`)
for every single class, making object allocation as trivial as grabbing
the last stored object from `AcediaObjectPool`'s internal dynamic array:
```unrealscript
// From `AcediaObjectPool` sources
public final function AcediaObject Fetch()
{
local AcediaObject result;
if (storedClass == none) return none;
if (objectPool.length <= 0) return none;
result = objectPool[objectPool.length - 1];
objectPool.length = objectPool.length - 1;
return result;
}
```
New pool is prepared for every class you create, as long as it is inherited
from `AcediaObject`.
`AcediaActor`s do not use object pools and are simply `Destroy()`ed.
### Detecting deallocated objects
Deallocated objects are not destroyed, but simply stored inside a special pool
to be later reused.
Problems can arise if some function deallocates your object without telling you.
If you suspect this might be the case or just want to make extra sure
your object is intact, then there are ways to confirm it.
First relevant method is defined in any class derived from
`AcediaObject` or `AcediaActor`: `IsAllocated()` that returns
`true` for objects that are currently allocated and `false` otherwise.
However, this method is not enough, since your object might be *reallocated*:
first deallocated and then allocated again by some other code.
Then `IsAllocate()` will return `true` even though your reference is
no longer valid.
This issue can be solved with *life version* - `int` value that changes
each time object is reallocated:
```unrealscript
local int lifeVersion;
local Text originalObject, newObject;
// Get object and remember its life version
originalObject = _.text.FromString("My string");
lifeVersion = originalObject.GetLifeVersion();
// Allocated objects always have positive life version
// and it won't change until they get deallocated
Log(originalObject.IsAllocated()); // true
Log(originalObject.GetLifeVersion() > 0); // true
Log(originalObject.GetLifeVersion() == lifeVersion); // true
// But after deallocation, life version will change and become negative
originalObject.FreeSelf();
Log(originalObject.IsAllocated()); // false
Log(originalObject.GetLifeVersion() > 0); // false
Log(originalObject.GetLifeVersion() == lifeVersion); // false
// This will reallocate object we've just deallocated
// and it will have different (positive) life version
newObject = _.text.FromString("New string!");
Log(originalObject == newObject); // true
Log(originalObject.IsAllocated()); // true
Log(originalObject.GetLifeVersion() > 0); // true
Log(originalObject.GetLifeVersion() == lifeVersion); // false
```
Summarizing, to detect whether your object was reallocated -
remember its life version value right after allocation
and then compare it to the `GetLifeVersion()`'s result.
Value returned by `GetLifeVersion()` changes after each reallocation
and won't repeat for the same object.
The only guarantee about life versions of deallocated objects is that they will
be negative.
### Customizing object pools for your classes
Object pool usage can be disabled completely for your class by setting
`usesObjectPool = false` in `defaultproperties` block.
Without object pools `_.memory.Allocate()` will create a new instance of
your class every single time.
You can also set a limit to how many objects will be stored in
an object pool with `defaultMaxPoolSize` variable.
Negative number (default for `AcediaObject`) means that object pool can
grow without a limit.
`0` effectively disables object pool, similar to setting
`usesObjectPool = false`.
However, this can be overwritten by server's settings
(see `AcediaSystem.ini: AcediaObjectPool`).