Anton Tarasenko
3 years ago
6 changed files with 1154 additions and 324 deletions
@ -0,0 +1,510 @@
|
||||
# Collections |
||||
|
||||
All Acedia's collections store `AcediaObject`s. By taking advantage of boxing |
||||
we can use them to store arbitrary types: |
||||
both value types (native variables and structs) |
||||
and reference types (`AcediaObject` and it's children). |
||||
|
||||
Currently Acedia provides dynamic arrays (regular integer-indexed array) |
||||
and associative arrays (collection of key-value pairs with quick access to |
||||
values via `AcediaObject` keys). |
||||
Using them is fairly straightforward, but, since they're dealing with objects, |
||||
some explanation about their memory management is needed. |
||||
Below we attempt to give a detailed description of everything you need to |
||||
efficiently use Acedia's collections. |
||||
|
||||
## Usage examples |
||||
|
||||
### Dynamic arrays |
||||
|
||||
Dynamics arrays can be created via either of |
||||
`_.collections.EmptyDynamicArray()` / `_.collections.NewDynamicArray()` methods. |
||||
`_.collections.NewDynamicArray()` takes an `array<Acedia>` argument and |
||||
populates returned `DynamicArray` with it's items, while |
||||
`_.collections.EmptyDynamicArray()` simply creates an empty `DynamicArray`. |
||||
|
||||
They are similar to regular dynamic `array<AcediaObject>`s with |
||||
several differences: |
||||
|
||||
1. They're passed by reference, rather than by value (no additional copies are |
||||
made when passing `DynamicArray` as an argument to a function or assigning it |
||||
to another variable); |
||||
2. It has richer interface; |
||||
3. It automatically handles necessary object deallocations. |
||||
|
||||
As an example to illustrate basic usage of `DynamicArray` let's create |
||||
a trivial class that remembers players' nicknames: |
||||
|
||||
```unrealscript |
||||
class PlayerDB extends AcediaObject; |
||||
|
||||
var private DynamicArray storage; |
||||
|
||||
// Constructor and destructor allow for memory management |
||||
protected function Constructor() |
||||
{ |
||||
storage = _.collections.EmptyDynamicArray(); |
||||
} |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
storage.FreeSelf(); |
||||
storage = none; |
||||
} |
||||
|
||||
public function RegisterNick(Text newNickName) |
||||
{ |
||||
if (newNickName == none) return; |
||||
// `Find` returns `-1` if object is not found |
||||
if (storage.Find(newNickName) >= 0) return; |
||||
storage.AddItem(newNickName); |
||||
} |
||||
|
||||
public function IsRegisteredID(Text toCheck) |
||||
{ |
||||
return (storage.Find(toCheck) >= 0); |
||||
} |
||||
|
||||
public function ForgetNick(Text toForget) |
||||
{ |
||||
// This method removes all instances of `toForget` in `storage`; |
||||
// Optionally there's a flag to only remove the first one. |
||||
storage.RemoveItem(toForget); |
||||
} |
||||
``` |
||||
|
||||
#### What happens if we deallocate stored objects? |
||||
|
||||
They will turn into `none`: |
||||
|
||||
```unrealscript |
||||
local Text item; |
||||
local DynamicArray storage; |
||||
storage = _.collections.EmptyDynamicArray(); |
||||
item = _.text.FromString("example"); |
||||
storage.AddItem(item); |
||||
// Everything is as expected here |
||||
TEST_ExpectNotNone(item); |
||||
TEST_ExpectNotNone(storage.GetItem(0)); |
||||
TEST_ExpectTrue(storage.GetItem(0) == item); |
||||
|
||||
// Now let's deallocate `item` |
||||
item.FreeSelf(); |
||||
|
||||
// Suddenly things are different: |
||||
TEST_ExpectNotNone(item); // `item` deallocated, but not dereferenced |
||||
TEST_ExpectNone(storage.GetItem(0)); // but it is gone from the collection |
||||
TEST_ExpectFalse(storage.GetItem(0) == item); |
||||
``` |
||||
|
||||
Let's explain what's changed after deallocation: |
||||
|
||||
1. Even though we've deallocated `item`, it's reference still points at |
||||
the `Text` object. |
||||
This is because deallocation is an Acedia's concept and actual UnrealScript |
||||
objects are not destroyed by it; |
||||
2. `storage.GetItem(0)` no longer points at that `Text` object. |
||||
Unlike a simple `array<AcediaObject>`, `DynamicObject` tracks status of it's |
||||
items and replaces their values with `none` when they're deallocated. |
||||
This kind of cleanup is something we cannot do with simple `FreeSelf()` or even |
||||
`_.memory.Deallocate()` for object stored in a regular array, but can for |
||||
objects stored in collections. |
||||
3. Since collection forgot about `item` after it was deallocated, |
||||
`storage.GetItem(0) == item` will be false. |
||||
|
||||
#### What happens if we remove an item from our `DynamicArray` collection? |
||||
|
||||
By default nothing - stored items will continue to exist outside the collection. |
||||
This is because by default `DynamicArray` (and `AssociativeArray`) is not |
||||
responsible for deallocation of its items. But it can be made to. |
||||
|
||||
Suppose that to avoid items disappearing from our collections, we put in their |
||||
copies instead. |
||||
For `Text` it can be accomplished with a simple `Copy()` method: |
||||
`storage.AddItem(item.Copy())`. |
||||
This creates a problem - `storage`, as we've explained before, won't actually |
||||
deallocate this item if we simply remove it. We will have to do so manually to |
||||
prevent memory leaks: |
||||
|
||||
```unrealscript |
||||
... |
||||
_.memory.Deallocate(storage.GetItem(i)); |
||||
storage.RemoveIndex(i); |
||||
``` |
||||
|
||||
which isn't ideal. |
||||
|
||||
To solve this problem we can add a copy of an `item` to our `DynamicArray` as |
||||
a *managed object*: collections will consider themselves responsible for |
||||
deallocation of objects marked as managed and will automatically clean them up. |
||||
To add an item as managed we need to simply specify second argument for |
||||
`AddItem(, true)` method: |
||||
|
||||
```unrealscript |
||||
local Text item; |
||||
local DynamicArray storage; |
||||
storage = _.collections.EmptyDynamicArray(); |
||||
item = _.text.FromString("example"); |
||||
storage.AddItem(item, true); |
||||
// Here added item is still allocated |
||||
TEST_ExpectTrue(item.IsAllocated()); |
||||
// But after it's removed from `storage`... |
||||
storage.RemoveIndex(0); |
||||
// ...it's automatically gets deallocated |
||||
TEST_ExpectFalse(item.IsAllocated()); |
||||
``` |
||||
|
||||
Whether you would want your collection to auto-deallocate your items or not |
||||
depends only on your needs. |
||||
|
||||
> **NOTE:** |
||||
> The same collection can contain both managed and unmanaged items. |
||||
|
||||
Let's rewrite `RegisterNick()` method of `PlayerDB` to make it independent from |
||||
whether `Text` objects passed to it are deallocated: |
||||
|
||||
```unrealscript |
||||
... |
||||
public function RegisterNick(Text newNickName) |
||||
{ |
||||
if (newNickName == none) return; |
||||
if (storage.Find(newNickName) >= 0) return; |
||||
// Store an independent, managed copy |
||||
storage.AddItem(newNickName.Copy(), true); |
||||
} |
||||
... |
||||
``` |
||||
|
||||
### Associative arrays |
||||
|
||||
> **IMPORTANT:** |
||||
> It is assumed you've read previous section about `DynamicArray`s and |
||||
> its managed objects first. |
||||
|
||||
Associative arrays allow to efficiently store and access `AcediaObject` values |
||||
via `AcediaObject` keys by using hash map under the hood. |
||||
While objects of any `AcediaObject`'s subclass can be used as keys, the main |
||||
reason for implementing associative arrays was to allow for `Text` keys and |
||||
examples in this sections will focus on them specifically. |
||||
|
||||
The basic interface is simple and can be demonstrated with this: |
||||
|
||||
```unrealscript |
||||
local AcediaObject item; |
||||
local AssociativeArray storage; |
||||
storage = _.collection.NewAssociativeArray(); |
||||
// Add some values |
||||
storage.SetItem(_.text.FromString("year"), _.ref.int(2021)); |
||||
storage.SetItem( _.text.FromString("comment"), |
||||
_.text.FromString("What year it is?")); |
||||
// Then get them |
||||
item = storage.GetItem(_.text.FromString("year")); |
||||
TEST_ExpectTrue(IntRef(item).Get() == 2021); |
||||
item = storage.GetItem(_.text.FromString("comment")); |
||||
TEST_ExpectTrue(Text(item).ToPlainString() == "What year it is?"); |
||||
``` |
||||
|
||||
In above example we've created separate text instances (with the same contents) |
||||
to store and retrieve items in `AssociativeArray`. |
||||
However it is inefficient to each time create `Text` anew: |
||||
|
||||
1. It defeats the purpose of using `Text` over `string`, since |
||||
(after initial creation cost) `Text` allows for a cheaper access to |
||||
individual characters and also allows us to compute `Text`'s hash only once, |
||||
caching it. |
||||
But if we create `Text` object every time we want to access value in |
||||
`AssociativeArray` we will only get more overhead without any benefits. |
||||
2. It leads to creation of useless objects, that we didn't deallocate in |
||||
the above example. |
||||
|
||||
So it is recommended that, whenever possible, your class would define reusable |
||||
`Text` constant that it would want to use as keys beforehand. |
||||
If you want to implement a class that receives zed's data as |
||||
an `AssociativeArray` and wants to buff its health, you can do the following: |
||||
|
||||
```unrealscript |
||||
class MyZedUpgrader extends AcediaObject; |
||||
|
||||
var protected Text TMAX_HEALTH; |
||||
|
||||
protected function StaticConstructor() |
||||
{ |
||||
default.TMAX_HEALTH = _.text.FromString("maxhealth"); |
||||
} |
||||
|
||||
public final function UpgradeMyZed(AssociativeArray zedData) |
||||
{ |
||||
local IntRef maxHealth; |
||||
maxHealth = IntRef(AssociativeArray.GetItem(TMAX_HEALTH)); |
||||
maxHealth.Set(maxHealth.Get() * 2); |
||||
} |
||||
``` |
||||
|
||||
[Text](./text.md) has more information about convenient ways to |
||||
efficiently create `Text` constants. |
||||
For example, in the above use case of upgrading zed's health it is acceptable to |
||||
do this instead: |
||||
|
||||
```unrealscript |
||||
class MyZedUpgrader extends AcediaObject; |
||||
|
||||
public final function UpgradeMyZed(AssociativeArray zedData) |
||||
{ |
||||
local IntRef maxHealth; |
||||
maxHealth = IntRef(AssociativeArray.GetItem(P("maxhealth"))); |
||||
maxHealth.Set(maxHealth.Get() * 2); |
||||
} |
||||
``` |
||||
|
||||
#### Memory management and `AssociativeArray` |
||||
|
||||
`AssociativeArray` supports the concept of managed objects in the same way as |
||||
`DynamicArray`s: by default objects are not managed, but can be added as such |
||||
when optional argument is used: |
||||
`AssociativeArray.GetItem(P("value"), someItem, true)`. |
||||
We'll just note here that it's possible to remove a managed item from |
||||
`AssociativeArray` without deallocating it with `TakeItem()`/`TakeEntry()` |
||||
methods. |
||||
|
||||
A question specific for `AssociativeArray`s is whether they deallocate |
||||
their keys. |
||||
And the answer is: they do not. |
||||
`AssociativeArray` will never deallocate its keys, even if a managed value is |
||||
recorded with them. |
||||
This way one can use the same pre-allocated key in several different |
||||
`AssociativeArray`s. |
||||
If you do need to deallocate them, you will have to do it manually. |
||||
|
||||
One good way to do so is to use `TakeEntry(AcediaObject key)` method that |
||||
returns a struct `Entry` with both key and recorded value inside: |
||||
|
||||
```unrealscript |
||||
struct Entry |
||||
{ |
||||
// Non-public fields are omitted |
||||
var public AcediaObject key; |
||||
var public AcediaObject value; |
||||
var public bool managed; |
||||
}; |
||||
``` |
||||
|
||||
This method also always removes stored value from `AssociativeArray` without |
||||
deallocating it, even if it was managed, making you responsible for it. |
||||
|
||||
In case of the opposite situation, where one deallocates an `AcediaObject` used |
||||
as a key, `AssociativeArray` will automatically remove appropriate entry |
||||
in its entirety. |
||||
However this is only a contingency measure: |
||||
**you should never deallocate objects that are still used as keys in `AssociativeArray`**. |
||||
One of the negative consequences is that it'll screw up `AssociativeArray`'s |
||||
`GetLength()` results, making it possibly overestimate the amount of |
||||
stored items (because there is no guarantee on *when* an entry with |
||||
deallocated key will be detected and cleaned up). |
||||
|
||||
#### Capacity |
||||
|
||||
Acedia's `AssociativeArray` works like a hash table and needs to allocate |
||||
sufficiently large dynamic array as a storage for its items. |
||||
If you keep adding new items that storage will eventually become too small for |
||||
hash table to work efficiently and we will have to reallocate and re-fill it. |
||||
If you want to add a huge enough amount of items into your `AssociativeArray`, |
||||
this process might be repeated several times. |
||||
This is not ideal, since it means doing a lot of iteration, each taking |
||||
noticeable time and increasing infinite loop counter |
||||
(game will crash if it gets high enough). |
||||
`AssociativeArray` allows you to set minimal capacity with |
||||
`SetMinimalCapacity()` method to force it to pre-allocate enough space for |
||||
the expected amount of items. |
||||
Setting minimal capacity to the maximum amount of items you expect to store in |
||||
the caller `AssociativeArray` can remove any need for reallocating the storage. |
||||
|
||||
> **NOTE:** |
||||
> `AssociativeArray` always allocates storage array with length of at least |
||||
> `MINIMUM_SIZE = 50` and won't need any reallocations before you add at least |
||||
> `MINIMUM_SIZE * MAXIMUM_DENSITY = 50 * 0.75 ~= 38` items, |
||||
> no matter the current minimal capacity |
||||
> (that can be checked with `GetMinimalCapacity()` method). |
||||
|
||||
#### [Advanced] Associative arrays' keys |
||||
|
||||
`AssociativeArray` allows to store `AcediaObject` values by `AcediaObject` keys. |
||||
Object of any class (derived from `AcediaObject`) can be used for either, but |
||||
behavior of the `AssociativeArray` regarding its key depends on how key's |
||||
`IsEqual()` and `GetHashCode()` methods are implemented. |
||||
|
||||
> **IMPORTANT:** |
||||
> [Refresh](../objects.md) your knowledge on how equality checks for |
||||
> Acedia's objects work, do not rely on intuition here. |
||||
|
||||
For example `Text`'s hash and equality is determined by its content: |
||||
|
||||
```unrealscript |
||||
local Text t1, t2; |
||||
t1 = _.text.FromString("Some random text"); |
||||
t2 = _.text.FromString("Some random text"); |
||||
// All of these assertions are correct: |
||||
TEST_ExpectTrue(t1.IsEqual(t2)); // same content |
||||
TEST_ExpectTrue(t1.GetHashCode() == t2.GetHashCode()); // same hashes |
||||
TEST_ExpectTrue(t1 != t2); // different objects |
||||
``` |
||||
|
||||
Therefore, if you used one `Text` as a key, then you will be able to obtain it's |
||||
value with another `Text` that contains the same `string`. |
||||
|
||||
However `MutableText`'s contents can change dynamically, so it cannot afford to |
||||
base its equality and hash on its contents: |
||||
|
||||
```unrealscript |
||||
local MutableText t1, t2; |
||||
t1 = _.text.FromStringM("Some random text"); |
||||
t2 = _.text.FromStringM("Some random text"); |
||||
// `IsEqual()` no longer compares contents; |
||||
// Use `Compare()` instead. |
||||
TEST_ExpectFalse(t1.IsEqual(t2)); |
||||
TEST_ExpectFalse(t1.GetHashCode() == t2.GetHashCode()); // different hashes (most likely) |
||||
TEST_ExpectTrue(t1 != t2); // different objects |
||||
``` |
||||
|
||||
`MutableText` can still be used as a key, but value stored with it will only be |
||||
obtainable by providing the exact instance of `MutableText`, regardless of |
||||
its contents: |
||||
|
||||
```unrealscript |
||||
local MutableText t1, t2; |
||||
local AssociativeArray storage; |
||||
storage = _.collection.NewAssociativeArray(); |
||||
t1 = _.text.FromStringM("Some random text"); |
||||
t2 = _.text.FromStringM("Some random text"); |
||||
storage.SetItem(t1, _.text.FromString("Contents!")); |
||||
TEST_ExpectNone(storage.GetItem(t2)); |
||||
TEST_ExpectNotNone(storage.GetItem(t1)); |
||||
``` |
||||
|
||||
As far as base Acedia's classes go, only `Text` and boxed |
||||
(immutable ones, not refs) values are a good fit to be used as |
||||
contents-dependent keys. |
||||
|
||||
## More accessors |
||||
|
||||
While you can store simple values inside these arrays in a straightforward |
||||
manner of `storage.SetItem(_.text.FromString("year"), _.ref.int(2021))`, |
||||
it is not very convenient. |
||||
Especially getting items from such arrays can be problematic, since that `int` |
||||
can potentially be stored as both immutable `IntBox` or mutable `IntRef`. |
||||
|
||||
To help with this problem Acedia's collections provide a bunch of convenience |
||||
accessors for UnrealScript's built-in types. |
||||
Let us start with getters |
||||
`GetBool()`, `GetByte()`, `GetInt()`, `GetFloat()`, `GetText()` |
||||
(since Acedia uses `Text` instead of `string`). |
||||
These take index for `DynamicArray` or `AcediaObject` keys for |
||||
`AssociativeArray` and return relevant type if they find either box or a ref |
||||
of such type in the caller array. |
||||
All of them, except `Text`, also allow you to provide default value as |
||||
a second argument - this value will be used if neither box or ref for |
||||
the desired type is found. |
||||
|
||||
Then there's setter methods |
||||
`SetBool()`, `SetByte()`, `SetInt()`, `SetFloat()` that take |
||||
at least two parameters: index/key and value to store. |
||||
They automatically create either box or ref object to wrap around passed |
||||
primitive value and always store it as a *managed item*. |
||||
Third, optional, `bool` parameter `asRef` allows you to decide whether passed |
||||
value should be saved inside the array in an immutable box or in a mutable ref |
||||
(default `false` is to save that primitive type in a box). |
||||
|
||||
> **NOTE:** |
||||
> There is no paired `SetText()` setter for `GetText()` getter, |
||||
> since `Text` itself is an object and can directly be saves with `SetItem()`. |
||||
|
||||
Here is an example of how they work: |
||||
|
||||
```unrealscript |
||||
local IntBox box; |
||||
local IntRef ref; |
||||
local DynamicArray storage; |
||||
storage = _.collection.NewDynamicArray(); |
||||
storage.SetInt(0, 7); |
||||
// `int` value is not returned normally, but there is not auto-conversion |
||||
// into `float` and so `GetFloat()` returns provided default value instead |
||||
Log("Value as int:" @ storage.GetInt(0)); // Value as int: 7 |
||||
Log("Value as float:" @ storage.GetFloat(0, 9)); // Value as int: 9 |
||||
|
||||
box = IntBox(storage.GetItem(0)); |
||||
// `int` should be stored in an allocated box |
||||
TEST_ExpectNotNone(box); |
||||
TEST_ExpectTrue(box.IsAllocated()); |
||||
// Re-recording `int` as ref causes previous box (managed by `storage`) |
||||
// to get destroyed |
||||
storage.SetInt(0, 11, true); |
||||
TEST_ExpectNotNone(box); // still not `none` |
||||
TEST_ExpectFalse(box.IsAllocated()); // but is not deallocated |
||||
Log("Value as int:" @ storage.GetInt(0)); // Value as int: 11 |
||||
// `int` should be stored in an allocated ref now |
||||
ref = IntRef(storage.GetItem(0)); |
||||
TEST_ExpectNotNone(ref); |
||||
TEST_ExpectTrue(ref.IsAllocated()); |
||||
``` |
||||
|
||||
## Even more accessors |
||||
|
||||
Collections `DynamicArray` and `AssociativeArray` are `AcediaObject`s themselves |
||||
and, therefore, can be stored in other arrays, producing hierarchical |
||||
structures, similar to those of JSON's arrays / objects. |
||||
|
||||
```json |
||||
{ |
||||
"main_guy": { |
||||
"status": "admin", |
||||
"maps": ["biotics", "bedlam", "waterworks"] |
||||
}, |
||||
"other_guy": { |
||||
"status": "random", |
||||
"maps": ["biotics", "westlondon"] |
||||
} |
||||
} |
||||
``` |
||||
|
||||
To access some variable, nested deep inside such structure, one can either |
||||
manually get reference of each collection on the way, e.g. to access second map |
||||
of the "other_guy" we'd need to first get reference to "other_guy"'s collection |
||||
(`AssociativeArray`): |
||||
|
||||
```json |
||||
{ |
||||
"status": "random", |
||||
"maps": ["biotics", "westlondon"] |
||||
} |
||||
``` |
||||
|
||||
then to the array of his maps (`DynamicArray`): |
||||
|
||||
```json |
||||
["biotics", "westlondon"] |
||||
``` |
||||
|
||||
and only then access second item. |
||||
This is too cumbersome! |
||||
Fortunately, Acedia's collections have an alternative solution: |
||||
|
||||
```unrealscript |
||||
userCollection.GetTextBy(P("/other_guy/maps/1")); // westlondon! |
||||
``` |
||||
|
||||
`/other_guy/maps/1` line is describes a path to the element nested deep inside |
||||
hierarchy of collections and follows the rules of a |
||||
[JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901). |
||||
Both `DynamicArray` and `AssociativeArray` support following methods that work |
||||
with such pointers: |
||||
`GetItemBy()`, `GetBoolBy()`, `GetByteBy()`, `GetIntBy()`, `GetFloatBy()`, |
||||
`GetTextBy()`, `GetDynamicArrayBy()` and `GetAssociativeArrayBy()`. |
||||
|
||||
Passing paths like `/other_guy/maps/1` requires collections to perform their |
||||
parsing every time such getter is called. |
||||
If you want to reuse the same path several times it might be better to convert |
||||
it into `JSONPointer` object (using `_.json.Pointer()` method) and then use |
||||
that object with following alternative methods: |
||||
`GetItemByJSON()`, `GetBoolByJSON()`, `GetByteByJSON()`, `GetIntByJSON()`, |
||||
`GetFloatByJSON()`, `GetTextByJSON()`, `GetDynamicArrayByJSON()`, |
||||
`GetAssociativeArrayByJSON()`. |
||||
This way parsing has to be done only once - when creating `JSONPointer` object. |
@ -0,0 +1,449 @@
|
||||
# Text support |
||||
|
||||
Acedia provides its own `Text` / `MutableText` classes for working with text |
||||
that are supposed to replace `string` variables as much as possible. |
||||
|
||||
Main reasons to forgo `string` in favor of custom text types are: |
||||
|
||||
1. `string` does not allow cheap access to either individual characters or |
||||
codepoints, which makes computing `string`'s hash too expensive; |
||||
2. Expanding `string`'s functionality without introducing new types would |
||||
require (for many cases) to disassemble it into codepoints and then to |
||||
assemble it back for each transformation; |
||||
3. Established way of defining characters' color for `string`s is inconvenient |
||||
to work with. |
||||
|
||||
These issues can be resolved with our new text types: `Text` and `MutableText`, |
||||
whose only difference is their mutability. |
||||
|
||||
> **NOTE:** |
||||
> `Text` and `MutableText` aren't yet in their finished state: |
||||
> using them is rather clunky compared to native `string`s and both their |
||||
> interface and implementation can be improved. While they already provide some |
||||
> important benefits, Acedia's insistence on replacing `string` with `Text` is |
||||
> more motivated by its supposed future, rather than current, state. |
||||
|
||||
## `string` |
||||
|
||||
Even if `Text`/`MutableText` are supposed to replace `string` variables, they |
||||
still have to be used to either produce `Text`/`MutableText` instances or |
||||
to store their values in config files. |
||||
This means we have to cover how Acedia deals with `string`s. |
||||
|
||||
### Colored vs plain strings |
||||
|
||||
**Colored strings** are normal UnrealScript `string`s that can contain |
||||
4-byte color changing sequences. Whenever some Acedia function takes |
||||
a *colored string* these color changing sequences are converted into formatting |
||||
information about color of its characters and are not treated |
||||
as separate symbols. |
||||
|
||||
> If you are unaware, 4-byte color changing sequences are defined as |
||||
> `<0x1b><red_byte><green_byte><blue_byte>` and they allow to color text that is |
||||
> being displayed by several native UnrealScript functions. |
||||
> For example, `string` that is defined as |
||||
> `"One word is colored" @ Chr(0x1b) $ Chr(1) $ Chr(255) $ Chr(1) $ "green"` |
||||
> will be output in game's console with its last word colored green. |
||||
> Red and blue bytes are taken as `1` instead of `0` because putting zero |
||||
> inside break the `string`. `10` is another value that leads to unexpected |
||||
> results and should be avoided. |
||||
|
||||
**Plain strings** are `string`s for which all contents are treated as their own |
||||
symbols. |
||||
If you pass a `string` with 4-byte color changing sequence to some method as |
||||
a *plain string*, these 4 bytes will also be treated as characters and |
||||
no color information will be extracted as a result. |
||||
|
||||
Plain strings are generally handled faster than colored strings. |
||||
|
||||
### Formatted strings |
||||
|
||||
Formatted `string`s are Acedia's addition and allow to define color information |
||||
in a more human-readable way than *colored strings*. |
||||
|
||||
To mark some part of a `string` to have a particular color you need to enclose |
||||
it into curly braces `{}`, specify color right after the opening brace |
||||
(without any spacing), then, after a single whitespace, must follow |
||||
the colored content. |
||||
For example, `"Each of these will be colored appropriately: {#ff0000 red}, {#00ff00 green}, {#0000ff blue}!"` |
||||
will correspond to a line |
||||
`Each of these will be colored appropriately: red, green, blue!` |
||||
and only three words representing colors will have any color defined for them. |
||||
|
||||
Color can be specified not only in hex format, but in also in one of |
||||
the more readable ways: `rgb(255,0,0)`, `rgb(r=0,G=255,b=255)`, |
||||
`rgba(r=45,g=167,b=32,a=200)`. |
||||
Or even using color aliases: |
||||
`"Each of these will be colored appropriately: {$red red}, {$green green}, {$blue blue}!"`. |
||||
|
||||
These formatting blocks can also be folded into each other: |
||||
`"Here {$purple is mostly purple, but {$red some parts} are {$yellow different} color}."` |
||||
with an arbitrary depth. |
||||
|
||||
### Conversion |
||||
|
||||
Various types of `string`s can be converted between each other by using |
||||
`Text` class, but do note that *formatted strings* can contain more information |
||||
than *colored strings* (since latter cannot simply close the colored segment) |
||||
and both of them can contain more information than *plain strings*, so |
||||
such conversion can lead to information loss. |
||||
Examples of conversion: |
||||
|
||||
```unrealscript |
||||
local Text auxiliary; |
||||
auxiliary = _.text.FromFormattedString("{$gold Hello}, {$crimson world}!"); |
||||
// Produces a string colored with 4-byte codes, a native way for UnrealScript |
||||
auxiliary.ToColoredString(); |
||||
// Strings all color and produces "Hello, world!" |
||||
auxiliary.ToPlainString(); |
||||
// Don't forget the cleanup! |
||||
_.memory.Free(auxiliary); |
||||
``` |
||||
|
||||
## `Character` |
||||
|
||||
`Character` describes a single symbol of a string and is a smallest text element |
||||
that can be returned from a `string` by Acedia's methods. |
||||
It contains data about what symbol it represents and what color it has. |
||||
`Character` can also be considered invalid, which means that it does not |
||||
represent any valid symbol. Validity can be checked with |
||||
`_.text.IsValidCharacter()` method. |
||||
|
||||
`Character` is defined as a structure with public fields |
||||
(necessary for the implementation), but you should not access them directly |
||||
if you wish for your code to stay compatible with future versions of Acedia. |
||||
|
||||
### `Formatting` |
||||
|
||||
Formatting describes how character should be displayed, which currently |
||||
corresponds to simply it's color (or the lack of it). |
||||
Formatting of a character can be accessed through |
||||
`_.text.GetCharacterFormatting()` method and changed |
||||
with `_.text.SetFormatting()`. |
||||
|
||||
It is a structure that contains two public fields, which can be freely accessed |
||||
(unlike `Character`'s fields): |
||||
|
||||
1. `isColored`: defines whether `Character` is even colored. |
||||
2. `color`: color of the `Character`. Only used if `isColored == true`. |
||||
|
||||
## `Text` and `MutableText` |
||||
|
||||
`Text` is an `AcediaObject` that must be appropriately allocated |
||||
(also deallocated) and is used by Acedia as substitute for a `string`. |
||||
It's contents are immutable: you can expect that they will not change if you |
||||
pass a `Text` as an argument to some method, although the whole object can |
||||
be deallocated. |
||||
`MutableText` is a child class of a `Text` that can change its own contents. |
||||
|
||||
To create either of them you can use `TextAPI` methods: |
||||
`_.text.Empty()` to create empty mutable text, |
||||
`_.text.FromString()` / `_.text.FromStringM()` to create immutable/mutable |
||||
text variants from a plain `string` and their analogues |
||||
`_.text.FromColoredString()` / `_.text.FromColoredStringM()` / |
||||
`_.text.FromFormattedString()` / `_.text.FromFormattedStringM()` |
||||
for colored and formatted `string`s. |
||||
|
||||
You can also get a `string` back by calling either of |
||||
`self.ToPlainString()` / `self.ToColoredString()` / `self.ToFormattedString()` |
||||
methods. |
||||
|
||||
To duplicate `Text` / `MutableText` themselves you can use `Copy()` |
||||
for immutable copies and `MutableCopy()` for mutable ones. |
||||
|
||||
## Defining `Text` / `MutableText` constants |
||||
|
||||
The major drawback of `Text` is how inconvenient it is to use it, compared to |
||||
simple string literals. It needs to be defined, allocated, used and |
||||
then deallocated: |
||||
|
||||
```unrealscript |
||||
local Text message; |
||||
message = _.text.FromString("Just some message to y'all!"); |
||||
_.console.ForAll().WriteLine(message) |
||||
.FreeSelf(); // Freeing console writer |
||||
message.FreeSelf(); // Freeing message |
||||
``` |
||||
|
||||
which can lead to some boilerplate code. Unfortunately, at this moment not much |
||||
can be done about this boilerplate. An ideal way to work with text literals |
||||
right now is to create `Text` instances with all the necessary text constants on |
||||
initialization and then use them: |
||||
|
||||
```unrealscript |
||||
class SomeClass extends AcediaObject; |
||||
|
||||
var Text MESSAGE, SPECIAL; |
||||
|
||||
protected function StaticConstructor() |
||||
{ |
||||
default.MESSAGE = _.text.FromString("Just some message to y'all!"); |
||||
default.SPECIAL = _.text.FromString("Only for special occasions!"); |
||||
} |
||||
|
||||
public final function DoSend() |
||||
{ |
||||
_.console.ForAll().WriteLine(MESSAGE).FreeSelf(); |
||||
} |
||||
|
||||
public final function DoSendSpecial() |
||||
{ |
||||
_.console.ForAll().WriteLine(SPECIAL).FreeSelf(); |
||||
} |
||||
``` |
||||
|
||||
Acedia also pre-defines `stringConstants` array that will be automatically |
||||
converted into an array of `Text`s that can later be accessed by their indices |
||||
through the `T()` method: |
||||
|
||||
```unrealscript |
||||
class SomeClass extends AcediaObject; |
||||
|
||||
var int TMESSAGE, TSPECIAL; |
||||
|
||||
public final function DoSend() |
||||
{ |
||||
_.console.ForAll().WriteLine(T(TMESSAGE)).FreeSelf(); |
||||
} |
||||
|
||||
public final function DoSendSpecial() |
||||
{ |
||||
_.console.ForAll().WriteLine(T(TSPECIAL)).FreeSelf(); |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
TMESSAGE = 0 |
||||
stringConstants(0) = "Just some message to y'all!" |
||||
TSPECIAL = 1 |
||||
stringConstants(1) = "Only for special occasions!" |
||||
} |
||||
``` |
||||
|
||||
This way of doing things is a bit more cumbersome, but is also safer in |
||||
the sense that `T()` will automatically allocate a new `Text` instance should |
||||
someone deallocate previous one: |
||||
|
||||
```unrealscript |
||||
local Text oldOne, newOne; |
||||
oldOne = T(TMESSAGE); |
||||
// `T()` returns the same instance of `Text` |
||||
TEST_ExpectTrue(oldOne == T(TMESSAGE)) |
||||
// Until we deallocate it... |
||||
oldOne.FreeSelf(); |
||||
// ...then it creates and returns newly allocated `Text` instance |
||||
newOne = T(TMESSAGE); |
||||
TEST_ExpectTrue(newOne.IsAllocated()); |
||||
|
||||
// This assertion *might* not actually be correct, since `newOne` can be |
||||
// just an `oldOne`, reallocated from the object pool. |
||||
// TEST_ExpectFalse(oldOne == newOne); |
||||
``` |
||||
|
||||
### An easier way |
||||
|
||||
While you should ideally define `Text` constants, setting them up can |
||||
get annoying. |
||||
To alleviate this issue Acedia provides three more methods for quickly |
||||
converting `string`s into `Text`: `P()` for plain `string`s, |
||||
`C()` for colored `string`s and `F()` for formatted `string`s. |
||||
With them out `SomeClass` can be rewritten as: |
||||
|
||||
```unrealscript |
||||
class SomeClass extends AcediaObject; |
||||
|
||||
public final function DoSend() |
||||
{ |
||||
_.console.ForAll().WriteLine(P("Just some message to y'all!")).FreeSelf(); |
||||
} |
||||
|
||||
public final function DoSendSpecial() |
||||
{ |
||||
_.console.ForAll().WriteLine(P("Only for special occasions!")).FreeSelf(); |
||||
} |
||||
``` |
||||
|
||||
They do not endlessly create `Text` instances, since they cache and reuse |
||||
the ones they return for the same `string`: |
||||
|
||||
```unrealscript |
||||
local Text firstInstance; |
||||
firstInstance = F("{$purple Some} {$red colored} {$yellow text}."); |
||||
// `F()` returns the same instance for the same `string` |
||||
TEST_ExpectTrue( firstInstance |
||||
== F("{$purple Some} {$red colored} {$yellow text}.")); |
||||
// But not for different one |
||||
TEST_ExpectFalse(firstInstance == F("Some other string")); |
||||
// Still the same |
||||
TEST_ExpectTrue( firstInstance |
||||
== F("{$purple Some} {$red colored} {$yellow text}.")); |
||||
``` |
||||
|
||||
Ideally one would at some point replace these calls with pre-defined constants, |
||||
but if you're using only a small amount of literals in your class, |
||||
then relying on them should be fine. However avoid using them for |
||||
an arbitrarily large amounts of `string`s, since as cache's size grows, |
||||
these methods will become increasingly less efficient: |
||||
|
||||
```unrealscript |
||||
// The more you call this method with different arguments, the worse |
||||
// performance gets since `C()` has to look `string`s up in |
||||
// larger and larger cache. |
||||
public function DisplayIt(string message) |
||||
{ |
||||
// This is bad, don't do this |
||||
_.console.ForAll().WriteLine(C(message)).FreeSelf(); |
||||
} |
||||
``` |
||||
|
||||
## Parsing |
||||
|
||||
Acedia provides some parsing functionality through a `Parser` class: |
||||
it must first be initialized by either `Initialize()` or `InitializeS()` method |
||||
(the only difference whether they take `Text` or `string` as a parameter) |
||||
and then it can parse passed contents by consuming its symbols from |
||||
the beginning to the end. |
||||
|
||||
For that it provides a set of *matcher methods* that try to read certain values |
||||
from the input. |
||||
For example, following can parse a color, defined in a hex format: |
||||
|
||||
```unrealscript |
||||
local Parser parser; |
||||
local int redComponent, greenComponent, blueComponent; |
||||
parser = _.text.ParseString("#23a405"); |
||||
parser.MatchS("#").MUnsignedInteger(redComponent, 16, 2) |
||||
.MUnsignedInteger(greenComponent, 16, 2) |
||||
.MUnsignedInteger(blueComponent, 16, 2); |
||||
// These should be correct values |
||||
TEST_ExpectTrue(redComponent == 35); |
||||
TEST_ExpectTrue(greenComponent == 164); |
||||
TEST_ExpectTrue(blueComponent == 5); |
||||
``` |
||||
|
||||
Here `MatchS()` matches an exact `string` constant and `MUnsignedInteger()` |
||||
matches an unsigned number (with base `16`) of length `2`, recording parsed |
||||
value into its first argument. |
||||
|
||||
Another example of parsing a color in format `rgb(123, 135, 2)`: |
||||
|
||||
```unrealscript |
||||
local Parser parser; |
||||
local int redComponent, greenComponent, blueComponent; |
||||
parser = _.text.ParseString("RGB( 123,135 , 2)"); |
||||
parser.MatchS("rgb(", SCASE_INSENSITIVE).Skip() |
||||
.MInteger(redComponent).Skip().MatchS(",").Skip() |
||||
.MInteger(greenComponent).Skip().MatchS(",").Skip() |
||||
.MInteger(blueComponent).Skip().MatchS(")"); |
||||
// These should be correct values |
||||
TEST_ExpectTrue(redComponent == 123); |
||||
TEST_ExpectTrue(greenComponent == 135); |
||||
TEST_ExpectTrue(blueComponent == 2); |
||||
TEST_ExpectTrue(parser.Ok()); |
||||
``` |
||||
|
||||
where `MInteger()` matches any decimal integer and then records that integer |
||||
into the first argument. `Skip()` matches a sequence of whitespaces of |
||||
an arbitrary length, adding some these calls allows this code to parse colors |
||||
defined with spacings between numbers and other characters like |
||||
`rgb( 12, 13 , 107 )`. `Ok()` method simply confirms that all matching calls |
||||
so far have succeeded. |
||||
|
||||
If you are unsure in which format the color was defined, then you can use |
||||
`Parser`'s methods for remembering/restoring a successful state: |
||||
you can first call `parser.Confirm()` to record that all the parsing so far |
||||
was successful and should not be discarded, then try to parse hex color. |
||||
After that: |
||||
|
||||
* If parsing was successful, - `parser.Ok()` check will return `true` and |
||||
you can call `parser.Confirm()` again to mark this new state as one that |
||||
shouldn't be discarded. |
||||
* Otherwise you can call `parser.R()` to reset your `parser` to the state it |
||||
was at the last `parser.Confirm()` call |
||||
(or the initial state if no `parser.Confirm()` calls were made) |
||||
and try parsing the color in some other way. |
||||
|
||||
```unrealscript |
||||
local Parser parser; |
||||
local int redComponent, greenComponent, blueComponent; |
||||
... |
||||
// Suppose we've successfully parsed something and |
||||
// need to parse color in one of the two forms next, |
||||
// so we remember the current state |
||||
parser.Confirm(); // This won't do anything if `parser` has already failed |
||||
// Try parsing color in it's rgb-form; |
||||
// It's not a major issue to have this many calls before checking for success, |
||||
// since once one of them has failed - others won't even try to do anything. |
||||
parser.MatchS("rgb(", SCASE_INSENSITIVE).Skip() |
||||
.MInteger(redComponent).Skip().MatchS(",").Skip() |
||||
.MInteger(greenComponent).Skip().MatchS(",").Skip() |
||||
.MInteger(blueComponent).Skip().MatchS(")"); |
||||
// If we've failed - try hex representation |
||||
if (!parser.Ok()) |
||||
{ |
||||
parser.R().MatchS("#") |
||||
.MUnsignedInteger(redComponent, 16, 2) |
||||
.MUnsignedInteger(greenComponent, 16, 2) |
||||
.MUnsignedInteger(blueComponent, 16, 2); |
||||
} |
||||
// It's fine to call `Confirm()` without checking for success, |
||||
// since it won't do anything for a parser in a failed state |
||||
parser.Confirm(); |
||||
``` |
||||
|
||||
>You can store even more different parser states with |
||||
`GetCurrentState()` / `RestoreState()` methods. |
||||
In fact, these are the ones used inside a lot of Acedia's methods to avoid |
||||
changing main `Parser`'s state that user can rely on. |
||||
|
||||
For more details and examples see the source code of `Parser.uc` or |
||||
any Acedia source code that uses `Parser`s. |
||||
|
||||
## JSON support |
||||
|
||||
> **NOTE:** |
||||
> This section is closely linked with [Collections](../API/Collections.md). |
||||
|
||||
Acedia's text capabilities also provide limited JSON support. |
||||
That is, Acedia can display some of it's types as JSON and parse any valid JSON |
||||
into its types/collections, but it does not guarantee verification of whether |
||||
parsed JSON is valid and can also accept some technically invalid JSON. |
||||
|
||||
Main methods for these tasks are `_.json.Print()`/`_.json.PrettyPrint()` and |
||||
`_.json.ParseWith()`, but there are some more type-specialized methods as well. |
||||
Here are the current rules of conversion from JSON to Acedia's types via |
||||
`_.json.ParseWith()`: |
||||
|
||||
1. Null values will be returned as `none`; |
||||
2. Number values will be return as an `IntBox`/`IntRef` if they consist |
||||
of only digits (and optionally a sign) and `FloatBox`/`FloatRef` |
||||
otherwise. Choice between box and ref is made based on |
||||
`parseAsMutable` parameter (boxes are immutable, refs are mutable); |
||||
3. String values will be parsed as `Text`/`MutableText`, based on |
||||
`parseAsMutable` parameter; |
||||
4. Array values will be parsed as a `DynamicArray`, it's items parsed |
||||
according to these rules (`parseAsMutable` parameter is propagated). |
||||
5. Object values will be parsed as a `AssociativeArray`, it's items |
||||
parsed according to these rules (`parseAsMutable` parameter is |
||||
propagated) and recorded under the keys parsed into `Text`. |
||||
|
||||
And printing with `_.json.Print()`/`_.json.PrettyPrint()` follows |
||||
symmetrical rules: |
||||
|
||||
1. `none` is printed into "null"; |
||||
2. Boolean types (`BoolBox`/`BoolRef`) are printed into JSON bool value; |
||||
3. Integer (`IntBox`/`IntRef`) and float (`FloatBox`/`FloatRef`) types |
||||
are printed into JSON number value; |
||||
4. `Text` and `MutableText` are printed into JSON string value; |
||||
5. `DynamicArray` is printed into JSON array with `Print()` method |
||||
applied to each of its items. If some of them have not printable |
||||
types - "none" will be used for them as a replacement. |
||||
6. `AssociativeArray` is printed into JSON object with `Print()` method |
||||
applied to each of it's items. Only items with `Text` keys are |
||||
printed, the rest is omitted. If some of them have not printable |
||||
types - "none" will be used for them as a replacement. |
||||
|
||||
The difference between `_.json.Print()` and `_.json.PrettyPrint()` is that |
||||
`_.json.Print()` prints out a minimal, compact json, while |
||||
`_.json.PrettyPrint()` prints a more human-readable JSON with indentation and |
||||
color highlights. |
@ -0,0 +1,125 @@
|
||||
# `UnrealAPI` |
||||
|
||||
Acedia tries to wrap a lot of base UnrealScript into its own types and classes |
||||
and avoids using classes like `PlayerController`, `Pawn` or types like `string` |
||||
as much as possible. |
||||
However sometimes it is necessary to work with those classes |
||||
(e.g. the needs of AcediaFixes module) |
||||
and `UnrealAPI` is an API that collects inside itself various convenience |
||||
methods for working with them. |
||||
|
||||
While ideally this API would cover all the facets relevant to base UnrealScript |
||||
functionality, it's not really feasible to set this goal at any of the Acedia's |
||||
milestones. |
||||
It covers what it covers and will be expanded little-by-little along |
||||
the development, driven mostly by the needs of Acedia itself. |
||||
Whoever is reading this is also welcome to suggest adding functionality |
||||
they need. |
||||
|
||||
## Connection service |
||||
|
||||
While not exactly a part of an API, a related `Service` is `ConnectionService` |
||||
that is responsible for tracking player connections to the server. |
||||
You can use this `Service` to obtain a list of current connections with |
||||
`GetActiveConnections()` method or track getting or losing connection with |
||||
`OnConnectionEstablished()` / `OnConnectionLost()` signal functions. |
||||
Appropriate handlers only take `Connection` struct as a parameter, |
||||
that describes connection in question (stores `PlayerController`, ip address |
||||
and hash - usually steam ID). |
||||
|
||||
You can get a link to an instance of `ConnectionService` the same way as with |
||||
any regular `Service`: |
||||
`ConnectionService(class'ConnectionService'.static.Require())`. |
||||
|
||||
## Functions and signal methods defined directly in `UnrealAPI` |
||||
|
||||
### Signals for `UnrealAPI` |
||||
|
||||
| Signal | Description | |
||||
|--------|-------------| |
||||
|`OnTick(float, float)` | Called every. Its parameters are in-game time, passed since last tick, and current game's speed (default value is always `1.0`, not `1.1`). | |
||||
|`OnDestructionFor()` | This signal method takes an additional `Actor` parameter. Handler added with `OnDestructionFor()` will be called when that `Atcor` is destroyed. | |
||||
|
||||
### Functions for `UnrealAPI` |
||||
|
||||
| Function | Description | |
||||
|----------|-------------| |
||||
|`GetLevel()` | Returns current game's `LevelInfo`. | |
||||
|`GetGameRI()` | Returns current game's `GameReplicationInfo`. | |
||||
|`GetKFGameRI()` | Returns current game's `GameReplicationInfo` as `KFGameReplicationInfo`. | |
||||
|`GetGameType()` | Returns current game's `GameInfo`. | |
||||
|`GetKFGameType()` | Returns current game's `GameInfo` as `KFGameType`. | |
||||
|`GetLocalPlayer()` | Returns current local player's `Controller`. | |
||||
|`GetInventoryFrom(class<Inventory>, Inventory, optional bool)` | Convenience method for finding a first inventory entry of the given class in the given inventory chain. | |
||||
|`GetAllInventoryFrom(class<Inventory>, Inventory, optional bool)` | Convenience method for finding a all inventory entries of the given class in the given inventory chain. | |
||||
|`ActorRef(optional Actor)` | Creates reference object to store a `Actor` value. | |
||||
|
||||
## Functions and signal related to `GameRules` |
||||
|
||||
`UnrealAPI` provides sub-API that can be accessed through `_.unreal.gameRules.`. |
||||
That API provides convenience methods for working with `GameRules` as well as |
||||
several signal functions for the `GameRules`'s events. |
||||
|
||||
### Signals for `GameRules` |
||||
|
||||
| Signal | Description | |
||||
|--------|-------------| |
||||
|`NavigationPoint OnFindPlayerStart(Controller, optional byte, optional string)` | Called when game decides on a player's spawn point. If a `NavigationPoint` is returned, signal propagation will be interrupted and returned value will be used as the player start. | |
||||
|`bool OnHandleRestartGame()` | Called in `GameInfo`'s `RestartGame()` method and allows to prevent game's restart. | |
||||
|`bool OnCheckEndGame(PlayerReplicationInfo, string)` | Allows modification of game ending conditions. Return `false` to prevent game from ending. | |
||||
|`bool OnCheckScore(PlayerReplicationInfo)` | Check if this score means the game ends. Return `true` to override `GameInfo`'s `CheckScore()`, or if game was ended (with a call to `Level.Game.EndGame()`). | |
||||
|`bool OnOverridePickupQuery(Pawn, Pickup, out byte)` | When pawn wants to pick something up, `GameRule`s are given a chance to modify it. If one of the `Slot`s returns `true`, `allowPickup` will determine if the object can be picked up. | |
||||
|`int OnNetDamage(int, int, Pawn, Pawn, Vector, out Vector, class<DamageType>)` | When pawn gets damaged, `GameRule`s are given a chance to modify that damage. | |
||||
|`bool OnPreventDeath(Pawn, Controller, class<DamageType>, Vector)` | When pawn is about to die, `GameRule`s are given a chance to prevent that. | |
||||
|`void OnScoreKill(Controller, Controller)` | Called when one `Pawn` kills another. | |
||||
|
||||
### Functions for `GameRules` |
||||
|
||||
| Function | Description | |
||||
|----------|-------------| |
||||
|`bool Add(class<GameRules>)` | Adds new `GameRules` class to the current `GameInfo`. Does nothing if give `GameRules` class was already added before. | |
||||
|`bool Remove(class<GameRules>)` | Removes given `GameRules` class from the current `GameInfo`, if they are active. Does nothing otherwise. | |
||||
|`GameRules FindInstance(class<GameRules>)` | Finds given class of `GameRules` if it's currently active in `GameInfo`. Returns `none` otherwise. | |
||||
|`bool AreAdded(class<GameRules>)` | Checks if given class of `GameRules` is currently active in `GameInfo`. | |
||||
|
||||
## Functions and signal related to `BroadcastHandler` |
||||
|
||||
`UnrealAPI` provides sub-API that can be accessed through `_.unreal.broadcast.`. |
||||
That API provides convenience methods for working with `BroadcastHandler`s |
||||
as well as several signal functions for the `BroadcastHandler`'s events. |
||||
|
||||
This API also defines auxiliary struct `LocalizedMessage` that consists of all |
||||
the parameters usually sent along with localized messages and |
||||
enum `InjectionLevel` that describes way of adding another `BroadcastHandler` |
||||
into the game. |
||||
|
||||
### Signals for `BroadcastHandler` |
||||
|
||||
| Signal | Description | |
||||
|--------|-------------| |
||||
|`bool OnBroadcastCheck(Actor, int)` | Called before text message is sent to any player, during the check for whether it is at all allowed to be broadcasted. Corresponds to the `HandlerAllowsBroadcast()` method from `BroadcastHandler`. Return `false` to prevent message from being broadcast. | |
||||
|`bool OnHandleText(Actor, out string, name, bool)` | Called before text message is sent to any player, but after the check for whether it is at all allowed to be broadcasted. Corresponds to the `Broadcast()` or `BroadcastTeam()` method from `BroadcastHandler` if `BHIJ_Root` injection method was used and to `BroadcastText()` for `BHIJ_Registered`. Return `false` to prevent message from being broadcast. | |
||||
|`bool OnHandleTextFor(PlayerController receiver, Actor sender, string, name)` | Called before text message is sent to a particular player. Corresponds to the `BroadcastText()` method from `BroadcastHandler`. Return `false` to prevent message from being broadcast to a specified player. | |
||||
|`bool OnHandleLocalized(Actor, LocalizedMessage)` | Called before localized message is sent to any player. Corresponds to the `AllowBroadcastLocalized()` method from `BroadcastHandler` if `BHIJ_Root` injection method was used and to `BroadcastLocalized()` for `BHIJ_Registered`. Return `false` to prevent message from being broadcast. | |
||||
|`bool OnHandleLocalizedFor(PlayerController receiver, Actor sender, LocalizedMessage)` | Called before localized message is sent to a particular player. Corresponds to the `BroadcastLocalized()` method from `BroadcastHandler`. Return `false` to prevent message from being broadcast to a specified player. | |
||||
|
||||
### Functions for `BroadcastHandler` |
||||
|
||||
| Function | Description | |
||||
|----------|-------------| |
||||
|`bool Add(class<BroadcastHandler>, optional InjectionLevel)` | Adds new `BroadcastHandler` class to the current `GameInfo`. Does nothing if given `BroadcastHandler` class was already added before. | |
||||
|`bool Remove(class<BroadcastHandler>)` | Removes given `BroadcastHandler` class from the current `GameInfo`, if it is active. Does nothing otherwise. | |
||||
|`BroadcastHandler FindInstance(class<BroadcastHandler>)` | Finds given class of `BroadcastHandler` if it's currently active in `GameInfo`. Returns `none` otherwise. | |
||||
|`bool IsAdded(class<GameRules>)` | Checks if given class of `BroadcastHandler` is currently active in `GameInfo`. | |
||||
|
||||
## Functions and signal related to `Mutator` |
||||
|
||||
`UnrealAPI` provides sub-API that can be accessed through `_.unreal.mutator.`. |
||||
That API provides a couple signal functions for the `Mutator`'s events. |
||||
|
||||
### Signals for `Mutator` |
||||
|
||||
| Signal | Description | |
||||
|--------|-------------| |
||||
|`bool OnCheckReplacement(Actor, out byte)` | Called whenever mutators (Acedia's mutator) is asked to check whether an `Actor` should be replaced. This check is done right after that `Actor` has spawned. | |
||||
|`OnMutate(string, PlayerController)` | Called on a server whenever a player uses a "mutate" console command. | |
@ -1,282 +0,0 @@
|
||||
# Text support |
||||
|
||||
Acedia provides it's own set of classes for working with text that is supposed to replace `string` variables as much as possible. |
||||
|
||||
Main reasons to forgo `string` in favor of custom text types are: |
||||
|
||||
1. `string` does not allow cheap access to either it's characters or codepoints, which makes necessary for Acedia operation of computing hash too expensive; |
||||
2. Expanding `string`'s functionality without introducing new types would require (for many cases) to disassemble it into codepoints and then to assemble it back for each transformation |
||||
3. Established way of defining characters' color for `string`s is inconvenient to work with; |
||||
4. `string` is reported to cause crashes when storing sufficiently huge values. |
||||
|
||||
All of these issues can be resolved by introducing new text types: `Text` and `MutableText`, whose only difference is their mutability (there is also `Character` values type for representing colored characters). |
||||
|
||||
> **NOTE:** `Text` and `MutableText` aren't yet in their finished state: using them is rather clunky compared to native `string`s and both their interface and implementation can be improved. While they already provide some important benefits, Acedia's insistence on replacing `string` with `Text` is more motivated by it's supposed future, rather than current, state. |
||||
|
||||
## `string` |
||||
|
||||
Even if `Text`/`MutableText` are supposed to replace `string` variables, they still have to be used to produce `Text`/`MutableText` instances, since we would like to use UnrealScript's string literals for that and load/save textual information in config files. |
||||
|
||||
Because of that we should first consider how Acedia treats `string` literals. |
||||
|
||||
### Colored vs plain strings |
||||
|
||||
**Colored strings** are pretty much normal UnrealScript `string`s that can contain 4-byte color changing sequences. Whenever some Acedia function takes a *colored string* these color changing sequences are converted into formatting information about color of it's characters and are not treated as separate symbols. |
||||
|
||||
> If you are unaware, 4-byte color changing sequences are defined as `<0x1b><red_byte><green_byte><blue_byte>` and they allow to color text that is being displayed by several native UnrealScript functions. For example, `string` that is defined as `"One word is colored" @ Chr(0x1b) $ Chr(1) $ Chr(255) $ Chr(1) $ "green"` will be output in game's console with it's last word colored green. Red and blue bytes are taken as `1` instead of `0` because it would otherwise break the `string`. `10` is another value that leads to unexpected results and should be avoided. |
||||
|
||||
**Plain strings** are `string`s, for which all contents are treated as their own symbols. If you pass a `string` with 4-byte color changing sequence to some method as a *plain string*, these 4 bytes will also be treated as characters and no color information will be extracted as a result. |
||||
|
||||
Plain strings are generally handled faster than colored strings. |
||||
|
||||
### Formatted strings |
||||
|
||||
Formatted `string`s are Acedia's addition and allow to define color information in a more human-readable way than *colored strings*. |
||||
|
||||
To mark some part of a `string` have a particular color you need to enclose it into curly braces `{}`, specify color right after the opening brace (without any spacing), then, after a single whitespace, must follow the colored content. For example, `"Each of these will be colored appropriately: {#ff0000 red}, {#00ff00 green}, {#0000ff blue}!"` will correspond to a line `Each of these will be colored appropriately: red, green, blue!` and only three words representing colors will have any color defined for them. |
||||
|
||||
Color can be specified not only in hex format, but in also in one of the more readable ways: `rgb(255,0,0)`, `rgb(r=0,G=255,b=255)`, `rgba(r=45,g=167,b=32,a=200)`. Or even using color aliases: `"Each of these will be colored appropriately: {$red red}, {$green green}, {$blue blue}!"`. |
||||
|
||||
These formatting blocks can also be folded into each other: `"Here {$purple is mostly purple, but {$red some parts} are {$yellow different} color}."` with an arbitrary depth. |
||||
|
||||
### Conversion |
||||
|
||||
Various types of strings can be converted between each other by using `Text` class, but do note that *formatted strings* can contain more information than *colored strings* (since latter cannot simply close the colored segment) and both of them can contain more information than *plain strings*, so such conversion can lead to information loss. |
||||
|
||||
## `Character` |
||||
|
||||
`Character` describes a single symbol of a string and is a smallest text element that can be returned from a `string` by Acedia's methods. It contains data about what symbol it represents and what color it has. `Character` can also be considered invalid, which means that it does not represent any valid symbol. Validity can be checked with `_.text.IsValidCharacter()` method. |
||||
|
||||
`Character` is defined as a structure with public fields (necessary for the implementation), but you should not access them directly if you wish your code to stay compatible with future versions of Acedia and to not break anything. |
||||
|
||||
### `Formatting` |
||||
|
||||
Formatting describes to how character should be displayed, which currently corresponds to simply it's color (or the lack of it). Formatting of a character can be accessed through `_.text.GetCharacterFormatting()` method and changed with `_.text.SetFormatting()`. |
||||
|
||||
It is a structure that contains two public fields, which can be freely accessed (unlike `Character`'s fields): |
||||
|
||||
1. `isColored`: defines whether `Character` is even colored. |
||||
2. `color`: color of the `Character`. Only used if `isColored == true`. |
||||
|
||||
## `Text` and `MutableText` |
||||
|
||||
`Text` is an `AcediaObject` that must be appropriately allocated and deallocated and is used inside Acedia as substitute to a `string` (any of the 3 types described above). It's contents are immutable: you can expect that they will not change if you pass a `Text` as an argument to some method (although the whole object can be deallocated). |
||||
|
||||
`MutableText` is a child class of a `Text` and can change it's own contents. |
||||
|
||||
To create either of them you can use `TextAPI` methods: `_.text.Empty()` to create empty mutable text, `_.text.FromString()` / `_.text.FromStringM()` to create immutable/mutable text variants from a plain `string` and their analogues `_.text.FromColoredString()` / `_.text.FromColoredStringM()` / `_.text.FromFormattedString()` / `_.text.FromFormattedStringM()` for colored and formatted `string`s. |
||||
|
||||
You can also get a `string` back by calling either of `self.ToPlainString()` / `self.ToColoredString()` / `self.ToFormattedString()` methods. |
||||
|
||||
To duplicate `Text` / `MutableText` themselves you can use `Copy()` for immutable and `MutableCopy()` for mutable copies. |
||||
|
||||
## Defining `Text` / `MutableText` constants |
||||
|
||||
The major drawback of `Text` is how inconvenient is using it, compared to simple string literals. It needs to be defined, allocated, used and then deallocated: |
||||
|
||||
```unrealscript |
||||
local Text message; |
||||
message = _.text.FromString("Just some message to y'all!"); |
||||
_.console.ForAll().WriteLine(message) |
||||
.FreeSelf(); // Freeing console writer |
||||
message.FreeSelf(); // Freeing message |
||||
``` |
||||
|
||||
which can lead to some boilerplate code. Unfortunately currently not much can be done about it. An ideal way to work with text literals right now is to create `Text` instances with all the necessary text constants on initialization and then use them: |
||||
|
||||
```unrealscript |
||||
class SomeClass extends AcediaObject; |
||||
|
||||
var Text MESSAGE, SPECIAL; |
||||
|
||||
protected function StaticConstructor() |
||||
{ |
||||
default.MESSAGE = _.text.FromString("Just some message to y'all!"); |
||||
default.SPECIAL = _.text.FromString("Only for special occasions!"); |
||||
} |
||||
|
||||
public final function DoSend() |
||||
{ |
||||
_.console.ForAll().WriteLine(MESSAGE).FreeSelf(); |
||||
} |
||||
|
||||
public final function DoSendSpecial() |
||||
{ |
||||
_.console.ForAll().WriteLine(SPECIAL).FreeSelf(); |
||||
} |
||||
``` |
||||
|
||||
Acedia also pre-defines `stringConstants` array that will be automatically converted into an array of `Text`s that can later be accessed by their indices through the `T()` method: |
||||
|
||||
```unrealscript |
||||
class SomeClass extends AcediaObject; |
||||
|
||||
var int TMESSAGE, TSPECIAL; |
||||
|
||||
public final function DoSend() |
||||
{ |
||||
_.console.ForAll().WriteLine(T(TMESSAGE)).FreeSelf(); |
||||
} |
||||
|
||||
public final function DoSendSpecial() |
||||
{ |
||||
_.console.ForAll().WriteLine(T(TSPECIAL)).FreeSelf(); |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
TMESSAGE = 0 |
||||
stringConstants(0) = "Just some message to y'all!" |
||||
TSPECIAL = 1 |
||||
stringConstants(1) = "Only for special occasions!" |
||||
} |
||||
``` |
||||
|
||||
This way of doing things is a bit more cumbersome, but is also safer in the sense that `T()` will automatically allocate a new `Text` instance should someone deallocate previous one: |
||||
|
||||
```unrealscript |
||||
local Text oldOne, newOne; |
||||
oldOne = T(TMESSAGE); |
||||
// `T()` returns the same instance of `Text` |
||||
TEST_ExpectTrue(oldOne == T(TMESSAGE)) |
||||
// Until we deallocate it... |
||||
oldOne.FreeSelf(); |
||||
// ...then it creates and returns newly allocated `Text` instance |
||||
newOne = T(TMESSAGE); |
||||
TEST_ExpectTrue(newOne.IsAllocated()); |
||||
|
||||
// This assertion *might* not actually be correct, since `newOne` can be |
||||
// just an `oldOne`, reallocated from the object pool. |
||||
// TEST_ExpectFalse(oldOne == newOne); |
||||
``` |
||||
|
||||
### An easier way |
||||
|
||||
While you should ideally define `Text` constants, making constants can get annoying when you are still in the process of writing code. To alleviate this issue Acedia provides three more methods for quickly converting `string`s into `Text`: `P()` for plain `string`s, `C()` for colored `string`s and `F()` for formatted `string`s. With them out `SomeClass` can be rewritten as: |
||||
|
||||
```unrealscript |
||||
class SomeClass extends AcediaObject; |
||||
|
||||
public final function DoSend() |
||||
{ |
||||
_.console.ForAll().WriteLine(P("Just some message to y'all!")).FreeSelf(); |
||||
} |
||||
|
||||
public final function DoSendSpecial() |
||||
{ |
||||
_.console.ForAll().WriteLine(P("Only for special occasions!")).FreeSelf(); |
||||
} |
||||
``` |
||||
|
||||
They do not endlessly create `Text` instances, since they cache and reuse the ones they return for the same `string`: |
||||
|
||||
```unrealscript |
||||
local Text firstInstance; |
||||
firstInstance = F("{$purple Some} {$red colored} {$yellow text}."); |
||||
// `F()` returns the same instance for the same `string` |
||||
TEST_ExpectTrue( firstInstance |
||||
== F("{$purple Some} {$red colored} {$yellow text}.")); |
||||
// But not for different one |
||||
TEST_ExpectTrue(firstInstance == F("Some other string")); |
||||
// Still the same |
||||
TEST_ExpectTrue( firstInstance |
||||
== F("{$purple Some} {$red colored} {$yellow text}.")); |
||||
``` |
||||
|
||||
Again, ideally one would at some point replace these calls with pre-defined constants, but if you're using only a small amount of literals in your class, then leaving them should be fine. However avoid using them for an arbitrarily large amounts of `string`s, since as cache grows, they will become increasingly less efficient: |
||||
|
||||
```unrealscript |
||||
// The more you call this method with different arguments, the worse |
||||
// performance gets since `C()` has to look `string`s up in |
||||
// larger and larger cache. |
||||
public function DisplayIt(string message) |
||||
{ |
||||
// This is bad |
||||
_.console.ForAll().WriteLine(C(message)).FreeSelf(); |
||||
} |
||||
``` |
||||
|
||||
## Parsing |
||||
|
||||
Acedia provides some parsing functionality through a `Parser` class: it must first be initialized by either `Initialize()` or `InitializeS()` method (the only difference whether they take `Text` or `string` as a parameter) and then it can parse passed contents by consuming (parsing) it's symbols in order: from the beginning to the end. |
||||
|
||||
For that it provides a set of matcher methods that try to read certain values from the input. For example, following can parse a color, defined in a hex format: |
||||
|
||||
```unrealscript |
||||
local Parser parser; |
||||
local int redComponent, greenComponent, blueComponent; |
||||
parser = _.text.ParseString("#23a405"); |
||||
parser.MatchS("#").MUnsignedInteger(redComponent, 16, 2) |
||||
.MUnsignedInteger(greenComponent, 16, 2) |
||||
.MUnsignedInteger(blueComponent, 16, 2); |
||||
// These should be correct values |
||||
TEST_ExpectTrue(redComponent == 35); |
||||
TEST_ExpectTrue(greenComponent == 164); |
||||
TEST_ExpectTrue(blueComponent == 5); |
||||
``` |
||||
|
||||
Here `MatchS()` matches an exact `string` constant and `MUnsignedInteger()` matches an unsigned number (with base `16`) of length `2`, recording parsed value into it's first argument. |
||||
|
||||
Another example of parsing a color in format `rgb(123, 135, 2)`: |
||||
|
||||
```unrealscript |
||||
local Parser parser; |
||||
local int redComponent, greenComponent, blueComponent; |
||||
parser = _.text.ParseString("RGB( 123,135 , 2)"); |
||||
parser.MatchS("rgb(", SCASE_INSENSITIVE).Skip() |
||||
.MInteger(redComponent).Skip().MatchS(",").Skip() |
||||
.MInteger(greenComponent).Skip().MatchS(",").Skip() |
||||
.MInteger(blueComponent).Skip().MatchS(")"); |
||||
// These should be correct values |
||||
TEST_ExpectTrue(redComponent == 123); |
||||
TEST_ExpectTrue(greenComponent == 135); |
||||
TEST_ExpectTrue(blueComponent == 2); |
||||
TEST_ExpectTrue(parser.Ok()); |
||||
``` |
||||
|
||||
where `MInteger()` matches any decimal integer and records it into it's first argument. Adding some `Skip()` calls that skip sequences of whitespace characters in between allows this code to parse colors defined with spacings between numbers and other characters. `Ok()` method simply confirms that all matching calls have succeeded. |
||||
|
||||
If you are unsure in which format the color was defined, then you can use `Parser`'s methods for remembering/restoring a successful state: you can first call `parser.Confirm()` to record that all the parsing so far was successful and should not be discarded, then try to parse hex color. After that: |
||||
|
||||
* If parsing was successful, - `parser.Ok()` check will return `true` and you can call `parser.Confirm()` again to mark this new state as one that shouldn't be discarded. |
||||
* Otherwise you can call `parser.R()` to reset your `parser` to the state it was at the last `parser.Confirm()` call and try parsing the color in some other way. |
||||
|
||||
```unrealscript |
||||
local Parser parser; |
||||
local int redComponent, greenComponent, blueComponent; |
||||
... |
||||
// Suppose we've successfully parsed something and |
||||
// need to parse color in one of the two forms next, |
||||
// so we remember the current state |
||||
parser.Confirm(); // This won't do anything if `parser` has already failed |
||||
// Try parsing color in it's rgb-form; |
||||
// It's not a major issue to have this many calls before checking for success, |
||||
// since once one of them has failed - others won't even try to do anything. |
||||
parser.MatchS("rgb(", SCASE_INSENSITIVE).Skip() |
||||
.MInteger(redComponent).Skip().MatchS(",").Skip() |
||||
.MInteger(greenComponent).Skip().MatchS(",").Skip() |
||||
.MInteger(blueComponent).Skip().MatchS(")"); |
||||
// If we've failed - try hex representation |
||||
if (!parser.Ok()) |
||||
{ |
||||
parser.R().MatchS("#") |
||||
.MUnsignedInteger(redComponent, 16, 2) |
||||
.MUnsignedInteger(greenComponent, 16, 2) |
||||
.MUnsignedInteger(blueComponent, 16, 2); |
||||
} |
||||
// It's fine to call `Confirm()` without checking for success, |
||||
// since it won't do anything for a parser in a failed state |
||||
parser.Confirm(); |
||||
``` |
||||
|
||||
>You can store even more different parser states with `GetCurrentState()` / `RestoreState()` methods. In fact, these are the ones used inside a lot of Acedia's methods to avoid changing main `Parser`'s state that user can rely on. |
||||
|
||||
For more details and examples see the source code of `Parser.uc` or any Acedia source code that uses `Parser`s. |
||||
|
||||
## JSON support |
||||
|
||||
> **NOTE:** This section is closely linked with [Collections](../API/Collections.md) |
||||
|
||||
Acedia's text capabilities also provide limited JSON support. That is, Acedia can display some of it's types as JSON and parse any valid JSON into it's types/collections, but it does not guarantee verification of whether parsed JSON is valid and can also accept some of technically invalid JSON. |
||||
|
||||
Main methods for these tasks are `_.json.Print()` and `_.json.ParseWith()`, but there are some more specialized methods as well. For precise types of data that can be converted to/from JSON refer to in-code documentation (roughly speaking it's `Text`, boxes, references and collections that contain them). |
Loading…
Reference in new issue