Browse Source

Refactor for Acedia v0.1.dev2

This is a giga commit, something that should not really be done with
git, but I messed up.

This commit brings a great amount of changes, most important is
reworking `TextAPI` and alsmost complete replacement of `string` with
`Text`/`MutableText`.

Another huge change is introduction of command system that allows to
define commands in a centralized manner, handles auto-parsing of
their parameters and auto generates help info.

Lastly, JSON data types were replaced with new
`DynamicArray`/`AssociativeArray` that are better designed and more
generic.
pull/8/head
Anton Tarasenko 4 years ago
parent
commit
7e99c8aa52
  1. 0
      config/AcediaAliases.ini
  2. 40
      config/AcediaAliases_Colors.ini
  3. 61
      config/AcediaSystem.ini
  4. 236
      docs/API/Collections.md
  5. 44
      docs/index.md
  6. 137
      docs/introduction/ObjectsActors.md
  7. 282
      docs/introduction/Text.md
  8. 225
      sources/AcediaObjectPool.uc
  9. 218
      sources/Aliases/AliasHash.uc
  10. 309
      sources/Aliases/AliasSource.uc
  11. 101
      sources/Aliases/Aliases.uc
  12. 129
      sources/Aliases/AliasesAPI.uc
  13. 3
      sources/Aliases/BuiltInSources/ColorAliasSource.uc
  14. 3
      sources/Aliases/BuiltInSources/ColorAliases.uc
  15. 3
      sources/Aliases/BuiltInSources/WeaponAliasSource.uc
  16. 3
      sources/Aliases/BuiltInSources/WeaponAliases.uc
  17. 1
      sources/Aliases/Tests/MockAliasSource.uc
  18. 1
      sources/Aliases/Tests/MockAliases.uc
  19. 106
      sources/Aliases/Tests/TEST_Aliases.uc
  20. 132
      sources/Color/ColorAPI.uc
  21. 328
      sources/Color/Tests/TEST_ColorAPI.uc
  22. 25
      sources/Commands/Aliases/CommandAliasSource.uc
  23. 26
      sources/Commands/Aliases/CommandAliases.uc
  24. 59
      sources/Commands/BroadcastListener_Commands.uc
  25. 103
      sources/Commands/BuiltInCommands/ACommandDosh.uc
  26. 101
      sources/Commands/BuiltInCommands/ACommandHelp.uc
  27. 38
      sources/Commands/BuiltInCommands/ACommandNick.uc
  28. 607
      sources/Commands/Command.uc
  29. 393
      sources/Commands/CommandCall.uc
  30. 883
      sources/Commands/CommandDataBuilder.uc
  31. 800
      sources/Commands/CommandParser.uc
  32. 158
      sources/Commands/Commands.uc
  33. 482
      sources/Commands/PlayersParser.uc
  34. 38
      sources/Commands/Tests/MockCommandA.uc
  35. 48
      sources/Commands/Tests/MockCommandB.uc
  36. 407
      sources/Commands/Tests/TEST_Command.uc
  37. 252
      sources/Commands/Tests/TEST_CommandDataBuilder.uc
  38. 49
      sources/Console/ConsoleAPI.uc
  39. 90
      sources/Console/ConsoleBuffer.uc
  40. 62
      sources/Console/ConsoleWriter.uc
  41. 29
      sources/CoreService.uc
  42. 540
      sources/Data/Collections/AssociativeArray.uc
  43. 83
      sources/Data/Collections/AssociativeArrayIterator.uc
  44. 51
      sources/Data/Collections/Collection.uc
  45. 98
      sources/Data/Collections/CollectionsAPI.uc
  46. 480
      sources/Data/Collections/DynamicArray.uc
  47. 80
      sources/Data/Collections/DynamicArrayIterator.uc
  48. 91
      sources/Data/Collections/Iter.uc
  49. 48
      sources/Data/Collections/Tests/MockItem.uc
  50. 356
      sources/Data/Collections/Tests/TEST_AssociativeArray.uc
  51. 322
      sources/Data/Collections/Tests/TEST_DynamicArray.uc
  52. 142
      sources/Data/Collections/Tests/TEST_Iterator.uc
  53. 948
      sources/Data/JSON/JArray.uc
  54. 1026
      sources/Data/JSON/JObject.uc
  55. 788
      sources/Data/JSON/JSON.uc
  56. 158
      sources/Data/JSON/JSONAPI.uc
  57. 1419
      sources/Data/JSON/Tests/TEST_JSON.uc
  58. 2
      sources/Feature.uc
  59. 60
      sources/Global.uc
  60. 2
      sources/Logger/LoggerAPI.uc
  61. 32
      sources/Manifest.uc
  62. 349
      sources/Memory/MemoryAPI.uc
  63. 67
      sources/Memory/MemoryService.uc
  64. 38
      sources/Memory/Tests/MockActor.uc
  65. 26
      sources/Memory/Tests/MockActorWithPool.uc
  66. 30
      sources/Memory/Tests/MockObject.uc
  67. 26
      sources/Memory/Tests/MockObjectNoPool.uc
  68. 245
      sources/Memory/Tests/TEST_Memory.uc
  69. 292
      sources/Players/APlayer.uc
  70. 4
      sources/Players/ConnectionListener_Player.uc
  71. 131
      sources/Players/PlayerService.uc
  72. 4
      sources/Service.uc
  73. 13
      sources/Singleton.uc
  74. 2
      sources/Testing/TestCase.uc
  75. 1097
      sources/Text/JSON/JSONAPI.uc
  76. 356
      sources/Text/MutableText.uc
  77. 631
      sources/Text/Parser.uc
  78. 501
      sources/Text/Tests/TEST_JSON.uc
  79. BIN
      sources/Text/Tests/TEST_Parser.uc
  80. BIN
      sources/Text/Tests/TEST_Text.uc
  81. BIN
      sources/Text/Tests/TEST_TextAPI.uc
  82. 150
      sources/Text/Tests/TEST_TextCache.uc
  83. 1228
      sources/Text/Text.uc
  84. 1073
      sources/Text/TextAPI.uc
  85. 208
      sources/Text/TextCache.uc
  86. 412
      sources/Types/AcediaActor.uc
  87. 418
      sources/Types/AcediaObject.uc
  88. 21
      sources/Types/Boxes/ArrayBox.uc
  89. 141
      sources/Types/Boxes/BoxAPI.uc
  90. BIN
      sources/Types/Boxes/Native/BoolArrayBox.uc
  91. BIN
      sources/Types/Boxes/Native/BoolBox.uc
  92. BIN
      sources/Types/Boxes/Native/ByteArrayBox.uc
  93. BIN
      sources/Types/Boxes/Native/ByteBox.uc
  94. BIN
      sources/Types/Boxes/Native/FloatArrayBox.uc
  95. BIN
      sources/Types/Boxes/Native/FloatBox.uc
  96. BIN
      sources/Types/Boxes/Native/IntArrayBox.uc
  97. BIN
      sources/Types/Boxes/Native/IntBox.uc
  98. BIN
      sources/Types/Boxes/Native/StringArrayBox.uc
  99. BIN
      sources/Types/Boxes/Native/StringBox.uc
  100. 56
      sources/Types/Boxes/ValueBox.uc
  101. Some files were not shown because too many files have changed in this diff Show More

0
config/AcediaAliases.ini

40
config/AcediaAliases_Colors.ini

@ -1,25 +1,25 @@
[AcediaCore_0_2.ColorAliasSource]
; System colors
record=(alias="text_default",value="rgb(255,255,255)")
record=(alias="text_subtle",value="rgb(128,128,128)")
record=(alias="text_emphasis",value="rgb(0,128,255)")
record=(alias="text_ok",value="rgb(0,255,0)")
record=(alias="text_warning",value="rgb(255,128,0)")
record=(alias="text_failure",value="rgb(255,0,0)")
record=(alias="type_number",value="rgb(181,137,0)")
record=(alias="type_boolean",value="rgb(38,139,210)")
record=(alias="type_string",value="rgb(211,54,130)")
record=(alias="type_literal",value="rgb(42,161,152)")
record=(alias="type_class",value="rgb(108,113,196)")
record=(alias="json_propertyName",value="rgb(255,255,255)")
record=(alias="json_objectBraces",value="rgb(128,128,128)")
record=(alias="json_arrayBraces",value="rgb(128,128,128)")
record=(alias="json_comma",value="rgb(128,128,128)")
record=(alias="json_colon",value="rgb(128,128,128)")
record=(alias="json_number",value="rgb(181,137,0)")
record=(alias="json_boolean",value="rgb(38,139,210)")
record=(alias="json_string",value="rgb(211,54,130)")
record=(alias="json_null",value="rgb(42,161,152)")
record=(alias="TextDefault",value="rgb(255,255,255)")
record=(alias="TextSubtle",value="rgb(128,128,128)")
record=(alias="TextEmphasis",value="rgb(0,128,255)")
record=(alias="TextOk",value="rgb(0,255,0)")
record=(alias="TextWarning",value="rgb(255,128,0)")
record=(alias="TextFailure",value="rgb(255,0,0)")
record=(alias="TypeNumber",value="rgb(255,235,172)")
record=(alias="TypeBoolean",value="rgb(199,226,244)")
record=(alias="TypeString",value="rgb(243,204,223)")
record=(alias="TypeLiteral",value="rgb(194,239,235)")
record=(alias="TypeClass",value="rgb(218,219,240)")
record=(alias="jPropertyName",value="rgb(255,255,255)")
record=(alias="jObjectBraces",value="rgb(128,128,128)")
record=(alias="jArrayBraces",value="rgb(128,128,128)")
record=(alias="jComma",value="rgb(128,128,128)")
record=(alias="jColon",value="rgb(128,128,128)")
record=(alias="jNumber",value="rgb(181,137,0)")
record=(alias="jBoolean",value="rgb(38,139,210)")
record=(alias="jString",value="rgb(211,54,130)")
record=(alias="jNull",value="rgb(42,161,152)")
; Pink colors
record=(alias="Pink",value="rgb(255,192,203)")
record=(alias="LightPink",value="rgb(255,182,193)")

61
config/AcediaSystem.ini

@ -1,4 +1,29 @@
; Every single option in this config should be considered [ADVANCED]
[AcediaCore_0_2.BroadcastEventsObserver]
; Acedia requires injecting it's own `BroadcastHandler` to listen to
; the broadcasted messages.
; It's normal for a mod to add it's own broadcast handler: broadcast handlers
; are implemented in such a way that they form a linked list and, after
; first (root) handler receives a message it tells about said message to
; the next handler, which does the same, propagating messages through
; the whole list.
; If you do not wish Acedia to add it's own handler, you should specify `0` as
; `usedInjectionLevel`'s value. If you want to allow it to simply add it's
; broadcast handler to the end of the handler's linked list, as described above,
; set it to `1`.
; However, more information can be obtained if Acedia's broadcast handler is
; inserted at the root of the whole chain. This is the prefered way for Acedia
; and if you do not have a reason to forbid it, you should leave this value
; at `2`.
usedInjectionLevel=2
[AcediaCore_0_2.Commands]
; This feature provides a mechanism to define commands that automatically
; parse their arguments into standard Acedia collection. It also allows to
; manage them (and specify limitation on how they can be called) in a
; centralized manner.
enableMe=true
[AcediaCore_0_2.AliasService]
; Changing these allows you to change in what sources `AliasesAPI`
; looks for weapon and color aliases.
@ -9,13 +34,6 @@ colorAliasesSource=Class'ColorAliasSource'
; Negative or zero values would be reset to `0.05`.
saveInterval=0.05
[AcediaCore_0_2.AliasHash]
; Reasonable lower and upper limits on hash table capacity for
; aliases' storage, that will be enforced if user requires something outside
; those bounds.
MINIMUM_CAPACITY=10
MAXIMUM_CAPACITY=100000
[AcediaCore_0_2.TestingService]
; Allows you to run tests on server's start up. This option is to help run
; tests quicker during development and should not be used for servers that are
@ -27,6 +45,9 @@ filterTestsByGroup=false
requiredName=""
requiredGroup=""
[AcediaCore_0_2.AcediaObjectPool]
;poolSizeOverwrite=(objectClass=<class>,maxPoolSize=<desiredPoolLimit>)
[AcediaCore_0_2.ConsoleAPI]
; These should guarantee decent text output in console even at
; 640x480 shit resolution
@ -34,32 +55,6 @@ requiredGroup=""
maxVisibleLineWidth=80
maxTotalLineWidth=108
[AcediaCore_0_2.JSON]
; Max precision that will be used when outputting JSON values as a string.
; Hardcoded to force this value between 0 and 10, inclusively.
MAX_FLOAT_PRECISION=4
[AcediaCore_0_2.JObject]
; Never use any hash table capacity below this limit,
; regardless of other variables
; (like `MINIMUM_CAPACITY` or `MINIMUM_DENSITY`).
ABSOLUTE_LOWER_CAPACITY_LIMIT=10
; Reasonable lower and upper limits on hash table capacity,
; that will be enforced if user requires something outside those bounds
MINIMUM_CAPACITY=50
MAXIMUM_CAPACITY=100000
; Minimum and maximum allowed density of elements
; (`storedElementCount / hashTable.length`).
; If density falls outside this range, - we have to resize hash table to
; get into (MINIMUM_DENSITY; MAXIMUM_DENSITY) bounds,
; as long as it does not violate other resctrictions.
MINIMUM_DENSITY=0.25
MAXIMUM_DENSITY=0.75
; Only ever reallocate hash table if new size will differ by
; at least that much, regardless of other restrictions.
MINIMUM_DIFFERENCE_FOR_REALLOCATION=50
[AcediaCore_0_2.ColorAPI]
; Changing these values will alter color's definitions in `ColorAPI`,
; changing how Acedia behaves

236
docs/API/Collections.md

@ -0,0 +1,236 @@
# 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 (indexed, variable sized array) and associative arrays (collection of key-value pairs with quick access to values via keys). Using them is fairly straightforward, but, since they're dealing with objects, some explanation about their memory management is needed. Next section aims to explain all that with some examples.
## 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. It's passed by reference, rather than by value (`DynamicArray` isn't copied each time it's passed as an argument to a function);
2. It has richer interface;
3. It can handle Acedia's object deallocation.
As an example to illustrate basic usage of `DynamicArray` let's create a trivial class that remembers players by their nickname:
```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);
TEST_ExpectNone(storage.GetItem(0));
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 `Text` object. This is because deallocation is an Acedia's convention 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 are deallocated. This cleanup is something we cannot do with simple `FreeSelf()` or even `_.memory.Deallocate()` for regular object, but can for objects stored in collections.
3. Since collection forgot about `item` after it was deallocated, `storage.GetItem(0) == item` will be false even if an instance of `item` will be later reused from it's object pool.
#### What happens if we deallocate our `DynamicArray` collection?
By default nothing.
To avoid items disappearing from our collections, we can put in their copies instead. For `Text` it can be accomplished with a simple `Copy()` method: `storage.AddItem(item.Copy())`. But this leads us to another problem - `storage` 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 do it for us. To add 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());
```
It depends on your needs whether you'd want your collection to auto-deallocate your items or not. Note also that 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;
storage.AddItem(newNickName.Copy(), true);
}
...
```
### Associative arrays
> **NOTE:** It is assumed you've read previous section about `DynamicArray`s and it's managed objects first.
Associative arrays allow to store and access `AcediaObject` values via `AcediaObject` keys 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 like so:
```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's inefficient to each time create `Text` anew:
1. It defeats the purpose of using `Text` over `string`, since one of `Text`'s main benefits is that once created, it allows cheaper access to individual characters and 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's recommended that, whenever possible, your class would define `Text` constant that it'd 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 it's 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](../instroduction/Text.md) has more information about how else you can efficiently create `Text` constants. For example, in the above use case of upgrading zed's health we can instead do this:
```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` support 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 it's keys, even if 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.
In case of the opposite situation, where one deallocates an `AcediaObject` used as a key: `AssociativeArray` will automatically remove appropriate entry in it's entirety. However this is only a clean-up attempt: **you should never deallocate objects that are still used as keys in `AssociativeArray`**. One of the negative consequences is that it'll screw up it's `GetLength()` results, making it possibly overestimate the amount of stored items (there is no guarantee on *when* an entry with deallocated key will be detected and disposed of).
### Associative array keys
`AssociativeArray` allows to store `AcediaObject` values by `AcediaObject` keys. Object of any class (derivative of `AcediaObject`) can be used for either, but behavior of the key depends on how their `IsEqual()` and `GetHashCode()` methods are implemented.
For example `Text`'s hash and equality is determined by it's 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 contents
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 and so it cannot afford to base it's equality and hash on it's 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 will only be obtainable by providing the exact instance of `MutableText` used as a key, no matter it's contents.
It's for the similar reason that immutable boxes are more fitting than mutable references as keys.

44
docs/index.md

@ -0,0 +1,44 @@
# Acedia Frameworks
Acedia Frameworks (later just Acedia) is a platform for modding Killing Floor, which includes both creating new mods and managing them on servers. It's main goals are to provide...
1. ...APIs that improve and standardize many recurring tasks like text coloring, storage and replication of arbitrary data (i.e. dynamic/associative arrays, player data), user commands, GUI and many more;
2. ...abstraction layer, allowing it to potentially switch "backend" from vanilla game to server perks or even something else;
3. ...out-of-the-box rich built-in capabilities to customize the game by changing settings;
4. ...simple, but powerful interface for managing and configuring Acedia on servers.
>**NOTE:** Although some note on server administration will be made, when relevant, this documentation is aimed for modders who want to use Acedia to make their mods.
## Introduction
First of all, note that much of Acedia's documentation is located in the source code itself: comments to classes and their methods. This documentation is meant as a more of the overview, it is supposed to provide necessary details, but expecting to duplicate full information between sources and these documents is unreasonable.
You can use this document to understand Acedia's capabilities and how everything fits together, but if you need a more detailed descriptions of Acedia's classes - you'll have to read documentation in source files.
### Global API access
Adding new global methods in UnrealScript isn't too convenient. One is forced to either use static methods through a cumbersome syntax: `class'MyAwesomeClass'.static.MyAwesomMethod(...)` or somehow fetch an instance of an object where desired functionality is defined.
Acedia doesn't ultimately fix this issue for everybody, but it addresses it for Acedia's own methods: any object in Acedia has a `_` variable defined inside it, which provides access to *API*s, i.e. objects that provide new functionality like so `_.text.ParseString("give@ $ebr")` or `_.color.RGB(132, 45, 12)`.
Concrete APIs are described below.
### Acedia and object management
Before tackling any other topic, it's important to understand that Acedia makes a much heavier use of `Object`s than most Killing Floor mods. This brings certain advantages, like allowing us to introduce collections capable of storing items of different classes, but also introduces an inconvenience of having to deallocate no longer used `Object`s.
[Read more](introduction/ObjectsActors.md)
### Acedia and text
Related to that is the question of `string`s. Acedia provides it's own type `Text` for storing textual data and it is intended to replace `string` almost everywhere. Main reason is that `string`s are monolithic and their individual characters are hard to access, which complicates implementing custom methods for working with them.
[Read more](introduction/Text.md)
## API
### Collections
Acedia provides dynamic and associative array collections capable of storing `AcediaObject`s. Thanks to boxing we can store essentially any type of variable inside them.
[Read more](API/Collections.md)

137
docs/introduction/ObjectsActors.md

@ -0,0 +1,137 @@
# Acedia's objects and actors
Acedia provides it's own base classes for objects and actors: `AcediaObject`/`AcediaActor` that allow a better integration into Acedia's infrastructure, by providing efficient means for their allocation/deallocation and access to Acedia-specific global methods.
## Allocation and deallocation
Any object (including actors) in Acedia must ultimately be allocated by `_.memory.Allocate()`. Although this method can be used to create object/actor of any class (including those that have nothing to do with Acedia), it does some additional initialization work for `AcediaObject` and `AcediaActor` that is necessary for them to function properly. Any allocated object, once no longer needed, must be deallocated via either `self.FreeSelf()` or `_.memory.Free()` methods.
Combination of these methods allows Acedia to store unused `AcediaObject`s in special pool and reuse them later when they are actually needed. This allows us to avoid creation of their excessive copies. However a care must be taken on a modder's side to properly manage such objects:
1. **You must never in any way use objects you have already deallocated;**
2. **You must not deallocate objects other that still might be used in other parts of the code.**
Acedia tries to minimize the need for manual allocation/deallocation and there're plans to potentially remove the need to care about it completely, but as of now it is impossible and is the necessary price for the benefits Acedia is making use of.
## Benefits
### Constructors and finalizers
Implementing and enforcing (de)allocation allowed us to introduce constructors that are guaranteed to be called after any Acedia's object is allocated as well as finalizers that are called when they are deallocated.
To make use of them one can simply overload protected methods `Constructor()` and `Finalizer()`. Parametrized constructors are not supported.
Static constructors are also supported (and can be used by overloading `StaticConstructor()`), - they are methods that take care of some initialization work before any instance of the given class is created. They are called only once, at the earliest of the following two events:
1. An instance of relevant class (or it's child class) is created;
2. Public `InitializeStatic()` method was called to force it early.
### General collections
Ability to allocate/deallocate objects in a way makes them cheaper: our ability to reuse them in theory means that we can keep allocating objects without worrying about having heaps of their old, now useless instances cluttering up our memory.
This, in turn, lets us use *boxes* - objects that we create to simply store primitive variables like `int`, `float` or `string` inside. This adds overhead on memory and time of accessing them, but it also allows us to store different types of variables in a single array `array<AcediaObject>`.
Following code illustrates it:
```unrealscript
local IntBox numberBox;
local BoolBox logicBox;
local array<AcediaObject> myArray;
// There two lines simply store `7` and `true` in boxes:
numberBox = _.box.int(7);
logicBox = _.box.bool(true);
// And now array our can stores both values:
myArray[0] = numberBox;
myArray[1] = numberBox;
```
While that example is artificial and of dubious usefulness, it lets Acedia provide:
1. `AssociativeArray` collection that implements hash map to store `AcediaObject`s with `AcediaObject` keys;
2. Method for parsing JSON of any complexity into regular Acedia's objects;
## Dangers
Title is *Dangers* but there's pretty much one main danger: someone (possibly you) will deallocate a certain object, but will then keep using it. So if you later allocate object of the same class, you can potentially share an instance with someone else, which can lead to all sorts of unpredictable bugs.
General recommendation to combat this is to take care to properly manage your resources. Always setting to `none` any object variables you've deallocated might help.
There are some ways to alleviate this issue for yourself:
1. Specifying an optional argument to allocation method: `_.memory.Allocate(..., true)` that forces it to create a new object;
2. Using object classes that have disabled object pool altogether (can be done with setting `usesObjectPool = false` in default properties).
3. If you are worried that some other piece of the code might deallocate your object without, you can check for it with life version. Each time an object (the same from UnrealScript's point of view) is re-allocated, it receives a unique to it *life version* that can be accessed by method `GetLifeVersion()`. You can use remember this value and if it's changed - then referenced object was deallocated at some point.
## Hash
To allow for implementation of hash maps (`AssociativeArray`), Acedia's objects and actors provide a `GetHashCode()` method that calculates their hash ([wiki](https://en.wikipedia.org/wiki/Hash_function)). For most objects it's simply an `int`, randomly generated upon their allocation.
There are, however, exceptions. For example, `Text`'s hash depends solely on it's contents, since they cannot be changed. Therefore two `Text`s, that store the same `string` value, will have the same hash, making them a good choice as a key of the hash map.
## Boxes, refs and hashing
Boxes and references (or just refs) are trivial wrappers around other variable types. For example here is full listing of `int`-reference (`IntRef`):
```unrealscript
class IntRef extends ValueRef;
var protected int value;
public final function int Get()
{
return value;
}
public final function IntRef Set(int newValue)
{
value = newValue;
return self;
}
public function bool IsEqual(Object other)
{
local IntRef otherBox;
otherBox = IntRef(other);
if (otherBox == none) {
return false;
}
return value == otherBox.value;
}
```
Box definition (`IntBox`) is similar. Benefit of boxes and refs is that they allow us to "transform" primitive types into the child type of `AcediaObject` (and, therefore, `Object` as well) and then store them in any collection that allows us to store `AcediaObject`s.
You can manually create box and ref type for any of your classes/structs, however it is currently cumbersome and a better way is planned in the future.
### Difference between boxes and refs
The difference is that boxes are immutable, once value has been recorded into them, it cannot be changed. This makes them inconvenient as variables, but instead allows their hash to depends only on their contents, which lets them be usable as keys for `AssociativeArray`. Generally, you want to default to using refs and only use boxes as key for `AssociativeArray`.
If you do need to use a box, - either create them using appropriate API (`_.box.`) or initialize them right after the allocation.
### Array boxes
Acedia provides not only boxes for primitive types themselves, but also for their arrays (for example `IntArrayBox`/`IntArrayRef`). Unlike standard UnrealScript arrays, they are passed by reference (because they are objects) and provide move functionality (like searching or, for array refs, sorting and inserting other arrays).
## [Technical] How allocation works
UnrealScript lacks any practical way to destroy an object on demand: the best one can do is remove any references to an object and wait for garbage collection (GC). But GC itself is too slow and causes noticeable lag spikes for players and can only seamlessly be used when switching between maps. There is a standard class `ObjectPool` that helps alleviate this problem by temporary storing unused references until they are needed. A reference to an instance of `ObjectPool` is provided by `LevelInfo`.
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 time a new object is requested from the pool, - allocation method has to search through all the other objects before finding an appropriate one. Modders can create their own `ObjectPool` instances specifically for their classes, but it's relatively cumbersome to reimplement each time.
Acedia automates this process by automatically providing each of it's classes with personal object pool. This extends to any child classes you create, as long as they were inherited from `AcediaObject` or `AcediaActor` at some point.
## [Technical] Customizing object pools for your classes
Object pool usage can be disabled completely for your class by setting `usesObjectPool = false`, which is the default for `AcediaActor`.
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 it can grow without a limit. `0` effectively disables object pool, similar to setting `usesObjectPool = false`. However do note that this variable can be overwritten by server's setting (see `AcediaSystem.ini: AcediaObjectPool`).
## [Technical] `AcediaObjectPool`
`AcediaObjectPool` is the class used for managing deallocated objects, derived from either `AcediaObject` or `AcediaActor`. For them it's created automatically and can be accessed by `_getPool()` static method.
However you can also make use of it for any kind of object. It has a simple interface and is not actually derived from either `AcediaObject` or `AcediaActor`, meaning that it can be created simply with a standard `new` operation. Just remember to first initialize it for a particular class you want to store with `Initialize()`.

282
docs/introduction/Text.md

@ -0,0 +1,282 @@
# 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).

225
sources/AcediaObjectPool.uc

@ -0,0 +1,225 @@
/**
* Acedia's implementation for object pool that can only store objects of
* one specific class to allow for their faster allocation.
* Allows to set a maximum capacity and can handle properly storing,
* auto-cleaning destroyed ones.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AcediaObjectPool extends Object
config(AcediaSystem);
// Class of objects that this `AcediaObjectPool` stores.
// if `== none`, - object pool is considered uninitialized.
var private class<Object> storedClass;
// Actual storage, functions on LIFO principle.
var private array<Object> objectPool;
// This struct and it's associated array `poolSizeOverwrite` allows
// server admins to rewrite the pool capacity for each class.
struct PoolSizeSetting
{
var class<Object> objectClass;
var int maxPoolSize;
};
var private config const array<PoolSizeSetting> poolSizeOverwrite;
// Capacity for object pool that we are using.
// Set during initialization and cannot be changed later.
var private int usedMaxPoolSize;
/**
* Initialize caller object pool to store objects of `initStoredClass` class.
*
* If successful, this action is irreversible: same pool cannot be
* re-initialized.
*
* @param initStoredClass Class of objects that caller object pool will store.
* @param forcedPoolSize Max pool size for the caller `AcediaObjectPool`.
* Leaving it at default `0` value will cause method to auto-determine
* the size: gives priority to the `poolSizeOverwrite` config array;
* if not specified, for `AcediaActor`s and `AcediaObject`s uses their
* `defaultMaxPoolSize` (ignoring `usesActorPool` setting),
* for other objects uses `-1`, to remove the capacity limit.
* @return `true` if initialization completed, `false` otherwise
* (including if it was already completed with passed `initStoredClass`).
*/
public final function bool Initialize(
class<Object> initStoredClass,
optional int forcedPoolSize)
{
if (storedClass != none) return false;
if (initStoredClass == none) return false;
// If does not matter that we've set those variables until
// we set `storedClass`.
if (forcedPoolSize == 0) {
usedMaxPoolSize = GetMaxPoolSizeForClass(initStoredClass);
}
else {
usedMaxPoolSize = forcedPoolSize;
}
if (usedMaxPoolSize == 0) {
return false;
}
storedClass = initStoredClass;
return true;
}
// Determines default object pool size for the initialization.
private final function int GetMaxPoolSizeForClass(class<Object> classToCheck)
{
local int i;
local int result;
local class<AcediaObject> classAsAcediaObject;
local class<AcediaActor> classAsAcediaActor;
// Get hard-coded value
classAsAcediaObject = class<AcediaObject>(classToCheck);
classAsAcediaActor = class<AcediaActor>(classToCheck);
if (classAsAcediaActor != none) {
result = classAsAcediaActor.default.defaultMaxPoolSize;
}
if (classAsAcediaObject != none) {
result = classAsAcediaObject.default.defaultMaxPoolSize;
}
else {
result = -1;
}
// Try to replace it with server's settings
for (i = 0; i < poolSizeOverwrite.length; i += 1) {
if (poolSizeOverwrite[i].objectClass == classToCheck) {
result = poolSizeOverwrite[i].maxPoolSize;
break;
}
}
return result;
}
/**
* Returns class of objects inside the caller `AcediaObjectPool`.
*
* @return class of objects inside caller the caller object pool;
* `none` means object pool was not initialized.
*/
public final function class<Object> GetClassOfStoredObjects()
{
return storedClass;
}
/**
* Clear the storage of all it's contents. In case of stored actors also
* destroys them.
*
* Can be used before UnrealEngine's garbage collection to free pooled objects.
*/
public final function Clear()
{
local int i;
local Actor nextActor;
if (storedClass != none) {
return;
}
if (class<Actor>(storedClass) == none)
{
// We can't destroy non-actors, so just get rid of references
objectPool.length = 0;
return;
}
for (i = 0; i < objectPool.length; i += 1)
{
nextActor = Actor(objectPool[i]);
if (nextActor != none) {
nextActor.Destroy();
}
}
objectPool.length = 0;
}
/**
* Adds object to the caller storage
* (that needs to be initialized to store `newObject.class` classes).
*
* @param newObject Object to put inside caller pool. Must be not `none` and
* have precisely the class this object pool was initialized to store.
* @return `true` on success and `false` on failure
* (can happen if passed `newObject` reference was invalid, caller storage
* is not initialized yet or reached it's capacity).
*/
public final function bool Store(Object newObject)
{
local int i;
if (newObject == none) return false;
if (newObject.class != storedClass) return false;
// Check for duplicates and clear dead references
while (i < objectPool.length)
{
if (objectPool[i] == newObject) {
return false;
}
// Getting `none` in object pool is expected to be rare and abnormal
// occurrence (since objects and actors put in the pool are
// not supposed to be touched), so filtering `none` values here
// should be negligible performance-wise, even if it's expensive.
if (objectPool[i] == none) {
objectPool.Remove(i, 1);
}
else {
i += 1;
}
}
if (usedMaxPoolSize >= 0 && objectPool.length < usedMaxPoolSize) {
return false;
}
objectPool[objectPool.length] = newObject;
return true;
}
/**
* Extracts last stored last not destroyed object (can happen for actors)
* from the pool.
*
* Returned object is no longer stored in the pool.
*
* @return Reference to the last (not destroyed) stored object.
* Only returns `none` if either empty or not initialized.
*/
public final function Object Fetch()
{
local int i;
local int validObjectIndex;
local Object result;
if (storedClass == none) {
return none;
}
validObjectIndex = -1;
for (i = objectPool.length - 1; i >= 0; i -= 1)
{
if (objectPool[i] == none) continue;
validObjectIndex = i;
break;
}
if (validObjectIndex < 0) {
return none;
}
result = objectPool[validObjectIndex];
objectPool.length = validObjectIndex;
return result;
}
defaultproperties
{
}

218
sources/Aliases/AliasHash.uc

@ -1,218 +0,0 @@
/**
* A class, implementing a hash-table-based dictionary for quick access to
* aliases' values.
* It does not support dynamic hash table capacity change and
* requires to set the size upfront.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AliasHash extends AcediaObject
dependson(AliasSource)
config(AcediaSystem);
// Reasonable lower and upper limits on hash table capacity,
// that will be enforced if user requires something outside those bounds
var private config const int MINIMUM_CAPACITY;
var private config const int MAXIMUM_CAPACITY;
// Bucket of alias-value pairs, with the same alias hash.
struct PairBucket
{
var array<AliasSource.AliasValuePair> pairs;
};
var private array<PairBucket> hashTable;
/**
* Initializes caller `AliasHash`.
*
* Calling this function again will clear all existing data and will create
* a brand new hash table.
*
* @param desiredCapacity Desired capacity of the underlying hash table.
* Will be clamped between `MINIMUM_CAPACITY` and `MAXIMUM_CAPACITY`.
* Not specifying anything as this parameter creates a hash table of
* size `MINIMUM_CAPACITY`.
* @return A reference to a caller object to allow for function chaining.
*/
public final function AliasHash Initialize(optional int desiredCapacity)
{
desiredCapacity = Clamp(desiredCapacity, MINIMUM_CAPACITY,
MAXIMUM_CAPACITY);
hashTable.length = 0;
hashTable.length = desiredCapacity;
return self;
}
// Helper method that is needed as a replacement for `%`, since it is
// an operation on `float`s in UnrealScript and does not have enough precision
// to work with hashes.
// Assumes positive input.
private function int Remainder(int number, int divisor)
{
local int quotient;
quotient = number / divisor;
return (number - quotient * divisor);
}
// Finds indices for:
// 1. Bucked that contains specified alias (`bucketIndex`);
// 2. Pair for specified alias in the bucket's collection (`pairIndex`).
// `bucketIndex` is always found,
// `pairIndex` is valid iff method returns `true`.
private final function bool FindPairIndices(
string alias,
out int bucketIndex,
out int pairIndex)
{
local int i;
local array<AliasSource.AliasValuePair> bucketPairs;
// `Locs()` is used because aliases are case-insensitive.
bucketIndex = _().text.GetHash(Locs(alias));
if (bucketIndex < 0) {
bucketIndex *= -1;
}
bucketIndex = Remainder(bucketIndex, hashTable.length);
// Check if bucket actually has given alias.
bucketPairs = hashTable[bucketIndex].pairs;
for (i = 0; i < bucketPairs.length; i += 1)
{
if (bucketPairs[i].alias ~= alias)
{
pairIndex = i;
return true;
}
}
return false;
}
/**
* Finds a value for a given alias.
*
* @param alias Alias for which we need to find a value.
* Aliases are case-insensitive.
* @param value If given alias is present in caller `AliasHash`, -
* it's value will be written in this variable.
* Otherwise value is undefined.
* @return `true` if we found value, `false` otherwise.
*/
public final function bool Find(string alias, out string value)
{
local int bucketIndex;
local int pairIndex;
if (FindPairIndices(alias, bucketIndex, pairIndex))
{
value = hashTable[bucketIndex].pairs[pairIndex].value;
return true;
}
return false;
}
/**
* Checks if caller `AliasHash` contains given alias.
*
* @param alias Alias to check for belonging to caller `AliasHash`.
* Aliases are case-insensitive.
* @return `true` if caller `AliasHash` contains the value for a given alias
* and `false` otherwise.
*/
public final function bool Contains(string alias)
{
local int bucketIndex;
local int pairIndex;
return FindPairIndices(alias, bucketIndex, pairIndex);
}
/**
* Inserts new record for alias `alias` for value of `value`.
*
* If there is already a value for a given `alias` - it will be overwritten.
*
* @param alias Alias to insert. Aliases are case-insensitive.
* @param value Value for a given alias to store.
* @return A reference to a caller object to allow for function chaining.
*/
public final function AliasHash Insert(string alias, string value)
{
local int bucketIndex;
local int pairIndex;
local AliasSource.AliasValuePair newRecord;
newRecord.value = value;
newRecord.alias = alias;
if (!FindPairIndices(alias, bucketIndex, pairIndex)) {
pairIndex = hashTable[bucketIndex].pairs.length;
}
hashTable[bucketIndex].pairs[pairIndex] = newRecord;
return self;
}
/**
* Inserts new record for alias `alias` for value of `value`.
*
* If there is already a value for a given `alias`, - new value will be
* discarded and `AliasHash` will not be changed.
*
* @param alias Alias to insert. Aliases are case-insensitive.
* @param value Value for a given alias to store.
* @param existingValue Value that will correspond to a given alias after
* this method's execution. If insertion was successful - given `value`,
* otherwise (if there already was a record for an `alias`)
* it will return value that already existed in caller `AliasHash`.
* @return `true` if given alias-value pair was inserted and `false` otherwise.
*/
public final function bool InsertIfMissing(
string alias,
string value,
out string existingValue)
{
local int bucketIndex;
local int pairIndex;
local AliasSource.AliasValuePair newRecord;
newRecord.value = value;
newRecord.alias = alias;
existingValue = value;
if (FindPairIndices(alias, bucketIndex, pairIndex)) {
existingValue = hashTable[bucketIndex].pairs[pairIndex].value;
return false;
}
pairIndex = hashTable[bucketIndex].pairs.length;
hashTable[bucketIndex].pairs[pairIndex] = newRecord;
return true;
}
/**
* Removes record, corresponding to a given alias `alias`.
*
* @param alias Alias for which all records must be removed.
* @return `true` if record was removed, `false` if id did not
* (can only happen when `AliasHash` did not have any records for `alias`).
*/
public final function bool Remove(string alias)
{
local int bucketIndex;
local int pairIndex;
if (FindPairIndices(alias, bucketIndex, pairIndex)) {
hashTable[bucketIndex].pairs.Remove(pairIndex, 1);
return true;
}
return false;
}
defaultproperties
{
MINIMUM_CAPACITY = 10
MAXIMUM_CAPACITY = 100000
}

309
sources/Aliases/AliasSource.uc

@ -5,7 +5,7 @@
* standard config ini-files.
* Several `AliasSource`s are supposed to exist separately, each storing
* aliases of particular kind: for weapon, zeds, colors, etc..
* Copyright 2020 Anton Tarasenko
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -25,10 +25,6 @@
class AliasSource extends Singleton
config(AcediaAliases);
// Name of the configurational file (without extension) where
// this `AliasSource`'s data will be stored.
var private const string configName;
// (Sub-)class of `Aliases` objects that this `AliasSource` uses to store
// aliases in per-object-config manner.
// Leaving this variable `none` will produce an `AliasSource` that can
@ -49,34 +45,29 @@ struct AliasValuePair
// Aliases data for saving and loading on a disk (ini-file).
// Name is chosen to make configurational files more readable.
var private config array<AliasValuePair> record;
// Hash table for a faster access to value by alias' name.
// Faster access to value by alias' name.
// It contains same records as `record` array + aliases from
// `loadedAliasObjects` objects when there are no duplicate aliases.
// Otherwise only stores first loaded alias.
var private AliasHash hash;
// How many times bigger capacity of `hash` should be, compared to amount of
// initially loaded data from a config.
var private const float HASH_TABLE_SCALE;
var private AssociativeArray aliasHash;
// Load and hash all the data `AliasSource` creation.
protected function OnCreated()
{
local int entriesAmount;
if (!AssertAliasesClassIsOwnedByMe()) {
if (!AssertAliasesClassIsOwnedByThisSource()) {
Destroy();
return;
}
// Load and hash
entriesAmount = LoadData();
hash = AliasHash(_.memory.Allocate(class'AliasHash'));
hash.Initialize(int(entriesAmount * HASH_TABLE_SCALE));
HashValidAliases();
loadedAliasObjects = aliasesClass.static.LoadAllObjects();
aliasHash = _.collections.EmptyAssociativeArray();
HashValidAliasesFromRecord();
HashValidAliasesFromPerObjectConfig();
}
// Ensures invariant of our `Aliases` class only belonging to us by
// itself ourselves otherwise.
private final function bool AssertAliasesClassIsOwnedByMe()
// Ensures that our `Aliases` class is properly linked with this
// source's class. Logs failure otherwise.
private final function bool AssertAliasesClassIsOwnedByThisSource()
{
if (aliasesClass == none) return true;
if (aliasesClass.default.sourceClass == class) return true;
@ -86,77 +77,110 @@ private final function bool AssertAliasesClassIsOwnedByMe()
return false;
}
// This method loads all the defined aliases from the config file and
// returns how many entries are there are total.
// Does not change data, including fixing duplicates.
private final function int LoadData()
// Load hashes from `AliasSource`'s config (`record` array)
private final function HashValidAliasesFromRecord()
{
local int i;
local int entriesAmount;
local array<string> objectNames;
entriesAmount = record.length;
if (aliasesClass == none) {
return entriesAmount;
}
objectNames =
GetPerObjectNames(configName, string(aliasesClass.name), MaxInt);
loadedAliasObjects.length = objectNames.length;
for (i = 0; i < objectNames.length; i += 1)
local Text aliasAsText, valueAsText;
for (i = 0; i < record.length; i += 1)
{
loadedAliasObjects[i] = new(none, objectNames[i]) aliasesClass;
entriesAmount += loadedAliasObjects[i].GetAliases().length;
aliasAsText = _.text.FromString(record[i].alias);
valueAsText = _.text.FromString(record[i].value);
InsertAlias(aliasAsText, valueAsText);
aliasAsText.FreeSelf();
valueAsText.FreeSelf();
}
return entriesAmount;
}
/**
* Simply checks if given alias is present in caller `AliasSource`.
*
* @param alias Alias to check, case-insensitive.
* @return `true` if present, `false` otherwise.
*/
public function bool ContainsAlias(string alias)
// Load hashes from `Aliases` objects' config
private final function HashValidAliasesFromPerObjectConfig()
{
return hash.Contains(alias);
local int i, j;
local Text nextValue;
local array<Text> valueAliases;
for (i = 0; i < loadedAliasObjects.length; i += 1)
{
nextValue = loadedAliasObjects[i].GetValue();
valueAliases = loadedAliasObjects[i].GetAliases();
for (j = 0; j < valueAliases.length; j += 1) {
InsertAlias(valueAliases[j], nextValue);
}
nextValue.FreeSelf();
_.memory.FreeMany(valueAliases);
}
}
// Inserts alias into `aliasHash`, cleaning previous keys/values in case
// they already exist.
// Takes care of lower case conversion to store aliases in `aliasHash`
// in a case-insensitive way.
private final function InsertAlias(Text alias, Text value)
{
local Text aliasLowerCaseCopy;
local AssociativeArray.Entry hashEntry;
if (alias == none) return;
if (value == none) return;
aliasLowerCaseCopy = alias.LowerCopy();
hashEntry = aliasHash.TakeEntry(aliasLowerCaseCopy);
if (hashEntry.value != none) {
LogDuplicateAliasWarning(alias, Text(hashEntry.value));
}
_.memory.Free(hashEntry.key);
_.memory.Free(hashEntry.value);
aliasHash.SetItem(aliasLowerCaseCopy, value.Copy(), true);
}
/**
* Tries to look up a value, stored for given alias in caller `AliasSource` and
* reports error upon failure.
* Checks if given alias is present in caller `AliasSource`.
*
* Also see `Try()` method.
*
* @param alias Alias, for which method will attempt to look up a value.
* Case-insensitive.
* @param value If passed `alias` was recorded in caller `AliasSource`,
* it's corresponding value will be written in this variable.
* Otherwise value is undefined.
* @return `true` if lookup was successful (alias present in 'AliasSource`)
* and correct value was written into `value`, `false` otherwise.
* @param alias Alias to check, case-insensitive.
* @return `true` if present, `false` otherwise.
*/
public function bool Resolve(string alias, out string value)
public function bool HasAlias(Text alias)
{
return hash.Find(alias, value);
local bool result;
local Text lowerCaseAlias;
if (alias == none) {
return false;
}
lowerCaseAlias = alias.LowerCopy();
result = aliasHash.HasKey(lowerCaseAlias);
lowerCaseAlias.FreeSelf();
return result;
}
/**
* Tries to look up a value, stored for given alias in caller `AliasSource` and
* silently returns given `alias` value upon failure.
* Return value stored for the given alias in caller `AliasSource`
* (as well as it's `Aliases` objects).
*
* Also see `Resolve()` method.
*
* @param alias Alias, for which method will attempt to look up a value.
* Case-insensitive.
* @return Value corresponding to a given alias, if it was present in
* caller `AliasSource` and value of `alias` parameter instead.
* @param alias Alias, for which method will attempt to return
* a value. Case-insensitive.
* @param copyOnFailure Whether method should return copy of original
* `alias` value in case caller source did not have any records
* corresponding to `alias`.
* @return If look up was successful - value, associated with the given
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
* flag: `copyOnFailure == false` means method will return `none`
* and `copyOnFailure == true` means method will return `alias.Copy()`.
* If `alias == none` method always returns `none`.
*/
public function string Try(string alias)
public function Text Resolve(Text alias, optional bool copyOnFailure)
{
local string result;
if (hash.Find(alias, result)) {
return result;
local Text result;
local Text lowerCaseAlias;
if (alias == none) {
return none;
}
return alias;
lowerCaseAlias = alias.LowerCopy();
result = Text(aliasHash.GetItem(lowerCaseAlias));
lowerCaseAlias.FreeSelf();
if (result != none) {
return result.Copy();
}
if (copyOnFailure) {
return alias.Copy();
}
return none;
}
/**
@ -164,7 +188,7 @@ public function string Try(string alias)
* If alias with the same name as `aliasToAdd` already exists, -
* method overwrites it.
*
* Can fail iff `aliasToAdd` is an invalid alias.
* Can fail iff `aliasToAdd` is an invalid alias or `aliasValue == none`.
*
* When adding alias to an object (`saveInObject == true`) alias `aliasToAdd`
* will be altered by changing any ':' inside it into a '.'.
@ -190,35 +214,30 @@ public function string Try(string alias)
* @return `true` if alias was added and `false` otherwise (alias was invalid).
*/
public final function bool AddAlias(
string aliasToAdd,
string aliasValue,
Text aliasToAdd,
Text aliasValue,
optional bool saveInObject)
{
local Text lowerCaseAlias;
local AliasValuePair newPair;
if (_.alias.IsAliasValid(aliasToAdd)) {
return false;
}
if (hash.Contains(aliasToAdd)) {
if (aliasToAdd == none) return false;
if (aliasValue == none) return false;
lowerCaseAlias = aliasToAdd.LowerCopy();
if (aliasHash.HasKey(lowerCaseAlias)) {
RemoveAlias(aliasToAdd);
}
// We might not be able to use per-object-config storage
if (saveInObject && aliasesClass == none) {
saveInObject = false;
_.logger.Warning("Cannot save alias in object for source `"
$ string(class)
$ "`, because it does not have appropriate `Aliases` class setup.");
}
// Save
if (saveInObject) {
GetAliasesObjectWithValue(aliasValue).AddAlias(aliasToAdd);
}
else
{
newPair.alias = aliasToAdd;
newPair.value = aliasValue;
newPair.alias = aliasToAdd.ToPlainString();
newPair.value = aliasValue.ToPlainString();
record[record.length] = newPair;
}
hash.Insert(aliasToAdd, aliasValue);
aliasHash.SetItem(lowerCaseAlias, aliasValue);
AliasService(class'AliasService'.static.Require()).PendingSaveSource(self);
return true;
}
@ -238,14 +257,23 @@ public final function bool AddAlias(
*
* @param aliasToRemove Alias that you want to remove from caller source.
*/
public final function RemoveAlias(string aliasToRemove)
public final function RemoveAlias(Text aliasToRemove)
{
local int i;
local bool isMatchingRecord;
local bool removedAliasFromRecord;
hash.Remove(aliasToRemove);
local AssociativeArray.Entry hashEntry;
if (aliasToRemove == none) {
return;
}
hashEntry = aliasHash.TakeEntry(aliasToRemove);
_.memory.Free(hashEntry.key);
_.memory.Free(hashEntry.value);
while (i < record.length)
{
if (record[i].alias ~= aliasToRemove)
isMatchingRecord = aliasToRemove
.CompareToPlainString(record[i].alias, SCASE_INSENSITIVE);
if (isMatchingRecord)
{
record.Remove(i, 1);
removedAliasFromRecord = true;
@ -264,107 +292,39 @@ public final function RemoveAlias(string aliasToRemove)
}
}
// Performs initial hashing of every record with valid alias.
// In case of duplicate or invalid aliases - method will skip them
// and log warnings.
private final function HashValidAliases()
{
if (hash == none) {
_.logger.Warning("Alias source `" $ string(class) $ "` called"
$ "`HashValidAliases()` function without creating an `AliasHasher`"
$ "instance first. This should not have happened.");
return;
}
HashValidAliasesFromRecord();
HashValidAliasesFromPerObjectConfig();
}
private final function LogDuplicateAliasWarning(
string alias,
string existingValue)
private final function LogDuplicateAliasWarning(Text alias, Text existingValue)
{
_.logger.Warning("Alias source `" $ string(class)
$ "` has duplicate record for alias \"" $ alias
$ "\". This is likely due to an erroneous config. \"" $ existingValue
$ "` has duplicate record for alias \"" $ alias.ToPlainString()
$ "\". This is likely due to an erroneous config. \""
$ existingValue.ToPlainString()
$ "\" value will be used.");
}
private final function LogInvalidAliasWarning(string invalidAlias)
private final function LogInvalidAliasWarning(Text invalidAlias)
{
_.logger.Warning("Alias source `" $ string(class)
$ "` contains invalid alias name \"" $ invalidAlias
$ "` contains invalid alias name \"" $ invalidAlias.ToPlainString()
$ "\". This alias will not be loaded.");
}
private final function HashValidAliasesFromRecord()
{
local int i;
local bool isDuplicate;
local string existingValue;
for (i = 0; i < record.length; i += 1)
{
if (!_.alias.IsAliasValid(record[i].alias))
{
LogInvalidAliasWarning(record[i].alias);
continue;
}
isDuplicate = !hash.InsertIfMissing(record[i].alias, record[i].value,
existingValue);
if (isDuplicate) {
LogDuplicateAliasWarning(record[i].alias, existingValue);
}
}
}
private final function HashValidAliasesFromPerObjectConfig()
{
local int i, j;
local bool isDuplicate;
local string existingValue;
local string objectValue;
local array<string> objectAliases;
for (i = 0; i < loadedAliasObjects.length; i += 1)
{
objectValue = loadedAliasObjects[i].GetValue();
objectAliases = loadedAliasObjects[i].GetAliases();
for (j = 0; j < objectAliases.length; j += 1)
{
if (!_.alias.IsAliasValid(objectAliases[j]))
{
LogInvalidAliasWarning(objectAliases[j]);
continue;
}
isDuplicate = !hash.InsertIfMissing(objectAliases[j], objectValue,
existingValue);
if (isDuplicate) {
LogDuplicateAliasWarning(objectAliases[j], existingValue);
}
}
}
}
// Tries to find a loaded `Aliases` config object that stores aliases for
// the given value. If such object does not exists - creates a new one.
private final function Aliases GetAliasesObjectWithValue(string value)
// Assumes `value != none`.
private final function Aliases GetAliasesObjectWithValue(Text value)
{
local int i;
local Text nextValue;
local Aliases newAliasesObject;
// This method only makes sense if this `AliasSource` supports
// per-object-config storage.
if (aliasesClass == none)
{
_.logger.Warning("`GetAliasesObjectForValue()` function was called for "
$ "alias source with `aliasesClass == none`."
$ "This should not happen.");
return none;
}
for (i = 0; i < loadedAliasObjects.length; i += 1)
{
if (loadedAliasObjects[i].GetValue() ~= value) {
nextValue = loadedAliasObjects[i].GetValue();
if (value.Compare(nextValue)) {
return loadedAliasObjects[i];
}
_.memory.Free(nextValue);
}
newAliasesObject = new(none, value) aliasesClass;
newAliasesObject = aliasesClass.static.LoadObject(value);
loadedAliasObjects[loadedAliasObjects.length] = newAliasesObject;
return newAliasesObject;
}
@ -372,8 +332,5 @@ private final function Aliases GetAliasesObjectWithValue(string value)
defaultproperties
{
// Source main parameters
configName = "AcediaAliases"
aliasesClass = class'Aliases'
// HashTable twice the size of data entries should do it
HASH_TABLE_SCALE = 2.0
}

101
sources/Aliases/Aliases.uc

@ -9,7 +9,7 @@
* which is necessary to allow storing aliases for class names via
* these objects (since UnrealScript's cannot handle '.'s in object's names
* in it's configs).
* Copyright 2020 Anton Tarasenko
* Copyright 2019 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -30,6 +30,10 @@ class Aliases extends AcediaObject
perObjectConfig
config(AcediaAliases);
// Name of the configurational file (without extension) where
// this `AliasSource`'s data will be stored.
var protected const string configName;
// Link to the `AliasSource` that uses `Aliases` objects of this class.
// To ensure that any `Aliases` sub-class only belongs to one `AliasSource`.
var public const class<AliasSource> sourceClass;
@ -38,17 +42,68 @@ var public const class<AliasSource> sourceClass;
// defined by this object's name `string(self.name)`.
var protected config array<string> alias;
// Since '.'s in values are converted into ':' for storage purposes,
// we need methods to convert between "storage" and "actual" value version.
// Since:
// 1. '.'s in values are converted into ':' for storage purposes;
// 2. We have to store values in `string` to make use of config files.
// we need methods to convert between "storage" (`string`)
// and "actual" (`Text`) value version.
// `ToStorageVersion()` and `ToActualVersion()` do that.
private final function string ToStorageVersion(string actualValue)
private final static function string ToStorageVersion(Text actualValue)
{
return Repl(actualValue, ".", ":");
return Repl(actualValue.ToPlainString(), ".", ":");
}
private final function string ToActualVersion(string storageValue)
// See comment to `ToStorageVersion()`.
private final static function Text ToActualVersion(string storageValue)
{
return Repl(storageValue, ":", ".");
return __().text.FromString(Repl(storageValue, ":", "."));
}
/**
* Loads all `Aliases` objects from their config file
* (defined in paired `AliasSource` class).
*
* @return Array of all `Aliases` objects, loaded from their config file.
*/
public static final function array<Aliases> LoadAllObjects()
{
local int i;
local array<string> objectNames;
local array<Aliases> loadedAliasObjects;
objectNames = GetPerObjectNames(default.configName,
string(default.class.name), MaxInt);
for (i = 0; i < objectNames.length; i += 1) {
loadedAliasObjects[i] = LoadObjectByName(objectNames[i]);
}
return loadedAliasObjects;
}
// Loads a new `Aliases` object by it's given name (`objectName`).
private static final function Aliases LoadObjectByName(string objectName)
{
local Aliases result;
// Since `MemoryAPI` for now does not support specifying names
// to created objects - do some manual dark magic and
// initialize this shit ourselves
result = new(none, objectName) default.class;
result._constructor();
return result;
}
/**
* Loads a new `Aliases` object based on the value (`aliasesValue`)
* of it's aliases.
*
* @param aliasesValue Value that aliases in this `Aliases` object will
* correspond to.
* @return Instance of `Aliases` object with a given name.
*/
public static final function Aliases LoadObject(Text aliasesValue)
{
if (aliasesValue != none) {
return LoadObjectByName(ToStorageVersion(aliasesValue));
}
return none;
}
/**
@ -56,7 +111,7 @@ private final function string ToActualVersion(string storageValue)
*
* @return Value, stored by this object.
*/
public final function string GetValue()
public final function Text GetValue()
{
return ToActualVersion(string(self.name));
}
@ -66,27 +121,37 @@ public final function string GetValue()
*
* @return Array of all aliases, stored by caller `Aliases` object.
*/
public final function array<string> GetAliases()
public final function array<Text> GetAliases()
{
return alias;
local int i;
local array<Text> textAliases;
for (i = 0; i < alias.length; i += 1) {
textAliases[i] = _.text.FromString(alias[i]);
}
return textAliases;
}
/**
* [For inner use by `AliasSource`] Adds new alias to this object.
*
* Does no duplicates checks through for it's `AliasSource` and
* neither it updates relevant `AliasHash`,
* neither does it update relevant `AliasHash`,
* but will prevent adding duplicate records inside it's own storage.
*
* @param aliasToAdd Alias to add to caller `Aliases` object.
* If `none`, method will do nothing.
*/
public final function AddAlias(string aliasToAdd)
public final function AddAlias(Text aliasToAdd)
{
local int i;
for (i = 0; i < alias.length; i += 1) {
if (alias[i] ~= aliasToAdd) return;
if (aliasToAdd == none) return;
for (i = 0; i < alias.length; i += 1)
{
if (aliasToAdd.CompareToPlainString(alias[i], SCASE_INSENSITIVE)) {
return;
}
}
alias[alias.length] = ToStorageVersion(aliasToAdd);
alias[alias.length] = aliasToAdd.ToPlainString();
AliasService(class'AliasService'.static.Require())
.PendingSaveObject(self);
}
@ -100,13 +165,14 @@ public final function AddAlias(string aliasToAdd)
*
* @param aliasToRemove Alias to remove from caller `Aliases` object.
*/
public final function RemoveAlias(string aliasToRemove)
public final function RemoveAlias(Text aliasToRemove)
{
local int i;
local bool removedAlias;
if (aliasToRemove == none) return;
while (i < alias.length)
{
if (alias[i] ~= aliasToRemove)
if (aliasToRemove.CompareToPlainString(alias[i], SCASE_INSENSITIVE))
{
alias.Remove(i, 1);
removedAlias = true;
@ -139,4 +205,5 @@ public final function SaveOrClear()
defaultproperties
{
sourceClass = class'AliasSource'
configName = "AcediaAliases"
}

129
sources/Aliases/AliasesAPI.uc

@ -1,6 +1,6 @@
/**
* Provides convenient access to Aliases-related functions.
* Copyright 2019 Anton Tarasenko
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -17,20 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AliasesAPI extends Singleton;
/**
* Checks that passed value is a valid alias name.
*
* A valid name is any name consisting out of 128 ASCII symbols.
*
* @param aliasToCheck Alias to check for validity.
* @return `true` if `aliasToCheck` is a valid alias and `false` otherwise.
*/
public final function bool IsAliasValid(string aliasToCheck)
{
return _.text.IsASCIIString(aliasToCheck);
}
class AliasesAPI extends AcediaObject;
/**
* Provides an easier access to the instance of the `AliasSource` of
@ -115,8 +102,8 @@ public final function AliasSource GetColorSource()
}
/**
* Tries to look up a value, stored for given alias in an `AliasSource`
* configured to store weapon aliases. Reports error on failure.
* Tries to look up a value stored for given alias in an `AliasSource`
* configured to store weapon aliases. Returns `none` on failure.
*
* Lookup of alias can fail if either alias does not exist in weapon alias
* source or weapon alias source itself does not exist
@ -124,55 +111,31 @@ public final function AliasSource GetColorSource()
* To determine if weapon alias source exists you can check
* `_.alias.GetWeaponSource()` value.
*
* Also see `TryWeapon()` method.
*
* @param alias Alias, for which method will attempt to look up a value.
* Case-insensitive.
* @param value If passed `alias` was recorded as a weapon alias,
* it's corresponding value will be written in this variable.
* Otherwise value is undefined.
* @return `true` if lookup was successful and `false` otherwise.
* @param alias Alias, for which method will attempt to
* look up a value. Case-insensitive.
* @param copyOnFailure Whether method should return copy of original
* `alias` value in case caller source did not have any records
* corresponding to `alias`.
* @return If look up was successful - value, associated with the given
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
* flag: `copyOnFailure == false` means method will return `none`
* and `copyOnFailure == true` means method will return `alias.Copy()`.
* If `alias == none` method always returns `none`.
*/
public final function bool ResolveWeapon(string alias, out string result)
public final function Text ResolveWeapon(
Text alias,
optional bool copyOnFailure)
{
local AliasSource source;
source = GetWeaponSource();
if (source != none) {
return source.Resolve(alias, result);
}
return false;
return source.Resolve(alias, copyOnFailure);
}
/**
* Tries to look up a value, stored for given alias in an `AliasSource`
* configured to store weapon aliases and silently returns given `alias`
* value upon failure.
*
* Lookup of alias can fail if either alias does not exist in weapon alias
* source or weapon alias source itself does not exist
* (due to either faulty configuration or incorrect definition).
* To determine if weapon alias source exists you can check
* `_.alias.GetWeaponSource()` value.
*
* Also see `ResolveWeapon()` method.
*
* @param alias Alias, for which method will attempt to look up a value.
* Case-insensitive.
* @return Weapon value corresponding to a given alias, if it was present in
* the weapon alias source and value of `alias` parameter instead.
*/
public function string TryWeapon(string alias)
{
local AliasSource source;
source = GetWeaponSource();
if (source != none) {
return source.Try(alias);
}
return alias;
return none;
}
/**
* Tries to look up a value, stored for given alias in an `AliasSource`
* Tries to look up a value stored for given alias in an `AliasSource`
* configured to store color aliases. Reports error on failure.
*
* Lookup of alias can fail if either alias does not exist in color alias
@ -181,51 +144,25 @@ public function string TryWeapon(string alias)
* To determine if color alias source exists you can check
* `_.alias.GetColorSource()` value.
*
* Also see `TryColor()` method.
*
* @param alias Alias, for which method will attempt to look up a value.
* Case-insensitive.
* @param value If passed `alias` was recorded as a color alias,
* it's corresponding value will be written in this variable.
* Otherwise value is undefined.
* @return `true` if lookup was successful and `false` otherwise.
*/
public final function bool ResolveColor(string alias, out string result)
{
local AliasSource source;
source = GetColorSource();
if (source != none) {
return source.Resolve(alias, result);
}
return false;
}
/**
* Tries to look up a value, stored for given alias in an `AliasSource`
* configured to store color aliases and silently returns given `alias`
* value upon failure.
*
* Lookup of alias can fail if either alias does not exist in color alias
* source or color alias source itself does not exist
* (due to either faulty configuration or incorrect definition).
* To determine if color alias source exists you can check
* `_.alias.GetColorSource()` value.
*
* Also see `ResolveColor()` method.
*
* @param alias Alias, for which method will attempt to look up a value.
* Case-insensitive.
* @return Color value corresponding to a given alias, if it was present in
* the color alias source and value of `alias` parameter instead.
* @param alias Alias, for which method will attempt to
* look up a value. Case-insensitive.
* @param copyOnFailure Whether method should return copy of original
* `alias` value in case caller source did not have any records
* corresponding to `alias`.
* @return If look up was successful - value, associated with the given
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
* flag: `copyOnFailure == false` means method will return `none`
* and `copyOnFailure == true` means method will return `alias.Copy()`.
* If `alias == none` method always returns `none`.
*/
public function string TryColor(string alias)
public final function Text ResolveColor(Text alias, optional bool copyOnFailure)
{
local AliasSource source;
source = GetColorSource();
if (source != none) {
return source.Try(alias);
return source.Resolve(alias, copyOnFailure);
}
return alias;
return none;
}
defaultproperties

3
sources/Aliases/BuiltInSources/ColorAliasSource.uc

@ -1,6 +1,6 @@
/**
* Source intended for color aliases.
* Copyright 2020 Anton Tarasenko
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -22,6 +22,5 @@ class ColorAliasSource extends AliasSource
defaultproperties
{
configName = "AcediaAliases_Colors"
aliasesClass = class'ColorAliases'
}

3
sources/Aliases/BuiltInSources/ColorAliases.uc

@ -1,6 +1,6 @@
/**
* Per-object-configuration intended for color aliases.
* Copyright 2020 Anton Tarasenko
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -23,5 +23,6 @@ class ColorAliases extends Aliases
defaultproperties
{
configName = "AcediaAliases_Colors"
sourceClass = class'ColorAliasSource'
}

3
sources/Aliases/BuiltInSources/WeaponAliasSource.uc

@ -1,6 +1,6 @@
/**
* Source intended for weapon aliases.
* Copyright 2020 Anton Tarasenko
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -22,6 +22,5 @@ class WeaponAliasSource extends AliasSource
defaultproperties
{
configName = "AcediaAliases_Weapons"
aliasesClass = class'WeaponAliases'
}

3
sources/Aliases/BuiltInSources/WeaponAliases.uc

@ -1,6 +1,6 @@
/**
* Per-object-configuration intended for weapon aliases.
* Copyright 2020 Anton Tarasenko
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -23,5 +23,6 @@ class WeaponAliases extends Aliases
defaultproperties
{
configName = "AcediaAliases_Weapons"
sourceClass = class'WeaponAliasSource'
}

1
sources/Aliases/Tests/MockAliasSource.uc

@ -22,6 +22,5 @@ class MockAliasSource extends AliasSource
defaultproperties
{
configName = "AcediaAliases_Tests"
aliasesClass = class'MockAliases'
}

1
sources/Aliases/Tests/MockAliases.uc

@ -23,5 +23,6 @@ class MockAliases extends Aliases
defaultproperties
{
configName = "AcediaAliases_Tests"
sourceClass = class'MockAliasSource'
}

106
sources/Aliases/Tests/TEST_Aliases.uc

@ -21,12 +21,6 @@ class TEST_Aliases extends TestCase
abstract;
protected static function TESTS()
{
Test_AliasHash();
Test_AliasLoading();
}
protected static function Test_AliasLoading()
{
Context("Testing loading aliases from a mock object `MockAliasSource`.");
SubTest_AliasLoadingCorrect();
@ -36,98 +30,50 @@ protected static function Test_AliasLoading()
protected static function SubTest_AliasLoadingCorrect()
{
local AliasSource source;
local string outValue;
Issue("`Resolve()` fails to return alias that should be loaded.");
source = _().alias.GetCustomSource(class'MockAliasSource');
TEST_ExpectTrue(source.Resolve("Global", outValue));
TEST_ExpectTrue(outValue == "value");
TEST_ExpectTrue(source.Resolve("ford", outValue));
TEST_ExpectTrue(outValue == "car");
Issue("`Try()` fails to return alias that should be loaded.");
TEST_ExpectTrue(source.Try("question") == "response");
TEST_ExpectTrue(source.Try("delorean") == "car");
Issue("`ContainsAlias()` reports alias, that should be present,"
Issue("`Resolve()` fails to return alias value that should be loaded.");
source = __().alias.GetCustomSource(class'MockAliasSource');
TEST_ExpectTrue(source.Resolve(P("Global")).ToPlainString() == "value");
TEST_ExpectTrue(source.Resolve(P("ford")).ToPlainString() == "car");
Issue("`Resolve()` fails to return passed alias after failure to"
@ "load it's value.");
TEST_ExpectTrue( source.Resolve(P("nothinMuch"), true).ToPlainString()
== "nothinMuch");
TEST_ExpectTrue( source.Resolve(P("random"), true).ToPlainString()
== "random");
Issue("`HasAlias()` reports alias, that should be present,"
@ "as missing.");
TEST_ExpectTrue(source.ContainsAlias("Global"));
TEST_ExpectTrue(source.ContainsAlias("audi"));
TEST_ExpectTrue(source.HasAlias(P("Global")));
TEST_ExpectTrue(source.HasAlias(P("audi")));
Issue("Aliases in per-object-configs incorrectly handle ':'.");
TEST_ExpectTrue(source.Try("HardToBeAGod") == "sci.fi");
TEST_ExpectTrue( source.Resolve(P("HardToBeAGod")).ToPlainString()
== "sci.fi");
Issue("Aliases with empty values in alias name or their value are handled"
@ "incorrectly.");
TEST_ExpectTrue(source.Try("") == "empty");
TEST_ExpectTrue(source.Try("also") == "");
TEST_ExpectTrue(source.Resolve(P("")).ToPlainString() == "empty");
TEST_ExpectTrue(source.Resolve(P("also")).ToPlainString() == "");
}
protected static function SubTest_AliasLoadingIncorrect()
{
local AliasSource source;
local string outValue;
Context("Testing loading aliases from a mock object `MockAliasSource`.");
Issue("`AliasAPI` cannot return value custom source.");
source = _().alias.GetCustomSource(class'MockAliasSource');
source = __().alias.GetCustomSource(class'MockAliasSource');
TEST_ExpectNotNone(source);
Issue("`Resolve()` reports success of finding inexistent alias.");
source = _().alias.GetCustomSource(class'MockAliasSource');
TEST_ExpectFalse(source.Resolve("noSuchThing", outValue));
Issue("`Try()` does not return given value for non-existent alias.");
TEST_ExpectTrue(source.Try("TheHellIsThis") == "TheHellIsThis");
Issue("`ContainsAlias()` reports inexistent alias as present.");
TEST_ExpectFalse(source.ContainsAlias("FordК"));
}
protected static function Test_AliasHash()
{
Context("Testing `AliasHasher`.");
SubTest_AliasHashInsertingRemoval();
}
protected static function SubTest_AliasHashInsertingRemoval()
{
local AliasHash hasher;
local string outValue;
hasher = new class'AliasHash';
hasher.Initialize();
Issue("`AliasHash` cannot properly store added aliases.");
hasher.Insert("alias", "value").Insert("one", "more");
TEST_ExpectTrue(hasher.Contains("alias"));
TEST_ExpectTrue(hasher.Contains("one"));
TEST_ExpectTrue(hasher.Find("alias", outValue));
TEST_ExpectTrue(outValue == "value");
TEST_ExpectTrue(hasher.Find("one", outValue));
TEST_ExpectTrue(outValue == "more");
Issue("`AliasHash` reports hashing aliases that never were hashed.");
TEST_ExpectFalse(hasher.Contains("alia"));
Issue("`AliasHash` cannot properly remove stored aliases.");
hasher.Remove("alias");
TEST_ExpectFalse(hasher.Contains("alias"));
TEST_ExpectTrue(hasher.Contains("one"));
TEST_ExpectFalse(hasher.Find("alias", outValue));
outValue = "wrong";
TEST_ExpectTrue(hasher.Find("one", outValue));
TEST_ExpectTrue(outValue == "more");
Issue("`InsertIfMissing()` function cannot properly store added aliases.");
TEST_ExpectTrue(hasher.InsertIfMissing("another", "var", outValue));
TEST_ExpectTrue(hasher.Find("another", outValue));
TEST_ExpectTrue(outValue == "var");
source = __().alias.GetCustomSource(class'MockAliasSource');
TEST_ExpectNone(source.Resolve(P("noSuchThing")));
Issue("`InsertIfMissing()` function incorrectly resolves a conflict with"
@ "an existing value.");
TEST_ExpectFalse(hasher.InsertIfMissing("one", "something", outValue));
TEST_ExpectTrue(outValue == "more");
Issue("`HasAlias()` reports inexistent alias as present.");
TEST_ExpectFalse(source.HasAlias(P("FordК")));
}
defaultproperties
{
caseName = "Aliases"
caseName = "AliasAPI"
caseGroup = "Aliases"
}

132
sources/Color/ColorAPI.uc

@ -20,7 +20,7 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ColorAPI extends Singleton
class ColorAPI extends AcediaObject
dependson(Parser)
config(AcediaSystem);
@ -44,6 +44,29 @@ enum ColorDisplayType
// Some useful predefined color values.
// They are marked as `config` to allow server admins to mess about with
// colors if they want to.
// System colors for displaying text and variables
var public config const Color TextDefault;
var public config const Color TextSubtle;
var public config const Color TextEmphasis;
var public config const Color TextOk;
var public config const Color TextWarning;
var public config const Color TextFailure;
var public config const Color TypeNumber;
var public config const Color TypeBoolean;
var public config const Color TypeString;
var public config const Color TypeLiteral;
var public config const Color TypeClass;
// Colors for displaying JSON values
var public config const Color jPropertyName;
var public config const Color jObjectBraces;
var public config const Color jArrayBraces;
var public config const Color jComma;
var public config const Color jColon;
var public config const Color jNumber;
var public config const Color jBoolean;
var public config const Color jString;
var public config const Color jNull;
// Pink colors
var public config const Color Pink;
var public config const Color LightPink;
@ -490,16 +513,20 @@ private final function Color ParseRGB(Parser parser)
local int blueComponent;
local Parser.ParserState initialParserState;
initialParserState = parser.GetCurrentState();
parser.Match("rgb(", true)
.MInteger(redComponent).Match(",")
.MInteger(greenComponent).Match(",")
.MInteger(blueComponent).Match(")");
parser.MatchS("rgb(", SCASE_INSENSITIVE)
.MInteger(redComponent).MatchS(",")
.MInteger(greenComponent).MatchS(",")
.MInteger(blueComponent).MatchS(")");
if (!parser.Ok())
{
parser.RestoreState(initialParserState).Match("rgb(", true)
.Match("r=", true).MInteger(redComponent).Match(",")
.Match("g=", true).MInteger(greenComponent).Match(",")
.Match("b=", true).MInteger(blueComponent).Match(")");
parser.RestoreState(initialParserState)
.MatchS("rgb(", SCASE_INSENSITIVE)
.MatchS("r=", SCASE_INSENSITIVE)
.MInteger(redComponent).MatchS(",")
.MatchS("g=", SCASE_INSENSITIVE)
.MInteger(greenComponent).MatchS(",")
.MatchS("b=", SCASE_INSENSITIVE)
.MInteger(blueComponent).MatchS(")");
}
return RGB(redComponent, greenComponent, blueComponent);
}
@ -513,18 +540,23 @@ private final function Color ParseRGBA(Parser parser)
local int alphaComponent;
local Parser.ParserState initialParserState;
initialParserState = parser.GetCurrentState();
parser.Match("rgba(", true)
.MInteger(redComponent).Match(",")
.MInteger(greenComponent).Match(",")
.MInteger(blueComponent).Match(",")
.MInteger(alphaComponent).Match(")");
parser.MatchS("rgba(", SCASE_INSENSITIVE)
.MInteger(redComponent).MatchS(",")
.MInteger(greenComponent).MatchS(",")
.MInteger(blueComponent).MatchS(",")
.MInteger(alphaComponent).MatchS(")");
if (!parser.Ok())
{
parser.RestoreState(initialParserState).Match("rgba(", true)
.Match("r=", true).MInteger(redComponent).Match(",")
.Match("g=", true).MInteger(greenComponent).Match(",")
.Match("b=", true).MInteger(blueComponent).Match(",")
.Match("a=", true).MInteger(alphaComponent).Match(")");
parser.RestoreState(initialParserState)
.MatchS("rgba(", SCASE_INSENSITIVE)
.MatchS("r=", SCASE_INSENSITIVE)
.MInteger(redComponent).MatchS(",")
.MatchS("g=", SCASE_INSENSITIVE)
.MInteger(greenComponent).MatchS(",")
.MatchS("b=", SCASE_INSENSITIVE)
.MInteger(blueComponent).MatchS(",")
.MatchS("a=", SCASE_INSENSITIVE)
.MInteger(alphaComponent).MatchS(")");
}
return RGBA(redComponent, greenComponent, blueComponent, alphaComponent);
}
@ -535,7 +567,7 @@ private final function Color ParseHexColor(Parser parser)
local int redComponent;
local int greenComponent;
local int blueComponent;
parser.Match("#")
parser.MatchS("#")
.MUnsignedInteger(redComponent, 16, 2)
.MUnsignedInteger(greenComponent, 16, 2)
.MUnsignedInteger(blueComponent, 16, 2);
@ -559,21 +591,25 @@ private final function Color ParseHexColor(Parser parser)
public final function bool ParseWith(Parser parser, out Color resultingColor)
{
local bool successfullyParsed;
local string colorAlias;
local Text colorContent;
local MutableText colorAlias;
local Parser colorParser;
local Parser.ParserState initialParserState;
if (parser == none) return false;
resultingColor.a = 0xff;
colorParser = parser;
initialParserState = parser.GetCurrentState();
if (parser.Match("$").MUntil(colorAlias,, true).Ok())
if (parser.MatchS("$").MUntil(colorAlias,, true).Ok())
{
colorParser = _.text.ParseString(_.alias.TryColor(colorAlias));
colorContent = _.alias.ResolveColor(colorAlias);
colorParser = _.text.Parse(colorContent);
initialParserState = colorParser.GetCurrentState();
_.memory.Free(colorContent);
}
else {
parser.RestoreState(initialParserState);
}
colorAlias.FreeSelf();
resultingColor = ParseRGB(colorParser);
if (!colorParser.Ok())
{
@ -601,18 +637,15 @@ public final function bool ParseWith(Parser parser, out Color resultingColor)
* @param resultingColor Parsed color will be written here if parsing is
* successful, otherwise value is undefined.
* If parsed color did not specify alpha component - 255 will be used.
* @param stringType How to treat given `string`,
* see `StringType` for more details.
* @return `true` if parsing was successful and false otherwise.
*/
public final function bool ParseString(
string stringWithColor,
out Color resultingColor,
optional Text.StringType stringType)
out Color resultingColor)
{
local bool successfullyParsed;
local Parser colorParser;
colorParser = _.text.ParseString(stringWithColor, stringType);
colorParser = _.text.ParseString(stringWithColor);
successfullyParsed = ParseWith(colorParser, resultingColor);
_.memory.Free(colorParser);
return successfullyParsed;
@ -641,31 +674,28 @@ public final function bool Parse(
return successfullyParsed;
}
/**
* Parses a color in any of the `ColorDisplayType` representations from the
* beginning of a given raw data.
*
* @param rawDataWithColor Raw data, that contains color definition at
* the beginning. Anything after color definition is not used.
* @param resultingColor Parsed color will be written here if parsing is
* successful, otherwise value is undefined.
* If parsed color did not specify alpha component - 255 will be used.
* @return `true` if parsing was successful and false otherwise.
*/
public final function bool ParseRaw(
array<Text.Character> rawDataWithColor,
out Color resultingColor)
{
local bool successfullyParsed;
local Parser colorParser;
colorParser = _.text.ParseRaw(rawDataWithColor);
successfullyParsed = ParseWith(colorParser, resultingColor);
_.memory.Free(colorParser);
return successfullyParsed;
}
defaultproperties
{
TextDefault=(R=255,G=255,B=255,A=255)
TextSubtle=(R=128,G=128,B=128,A=255)
TextEmphasis=(R=0,G=128,B=255,A=255)
TextOk=(R=0,G=255,B=0,A=255)
TextWarning=(R=255,G=128,B=0,A=255)
TextFailure=(R=255,G=0,B=0,A=255)
TypeNumber=(R=255,G=235,B=172,A=255)
TypeBoolean=(R=199,G=226,B=244,A=255)
TypeString=(R=243,G=204,B=223,A=255)
TypeLiteral=(R=194,G=239,B=235,A=255)
TypeClass=(R=218,G=219,B=240,A=255)
jPropertyName=(R=255,G=255,B=255,A=255)
jObjectBraces=(R=128,G=128,B=128,A=255)
jArrayBraces=(R=128,G=128,B=128,A=255)
jComma=(R=128,G=128,B=128,A=255)
jColon=(R=128,G=128,B=128,A=255)
jNumber=(R=181,G=137,B=0,A=255)
jBoolean=(R=38,G=139,B=210,A=255)
jString=(R=211,G=54,B=130,A=255)
jNull=(R=42,G=161,B=152,A=255)
Pink=(R=255,G=192,B=203,A=255)
LightPink=(R=255,G=182,B=193,A=255)
HotPink=(R=255,G=105,B=180,A=255)

328
sources/Color/Tests/TEST_ColorAPI.uc

@ -42,7 +42,7 @@ protected static function SubTest_ColorCreationRGB()
local Color createdColor;
Issue("`RGB() function does not set red, green and blue components"
@ "correctly.");
createdColor = _().color.RGB(145, 67, 237);
createdColor = __().color.RGB(145, 67, 237);
TEST_ExpectTrue(createdColor.r == 145);
TEST_ExpectTrue(createdColor.g == 67);
TEST_ExpectTrue(createdColor.b == 237);
@ -53,7 +53,7 @@ protected static function SubTest_ColorCreationRGB()
Issue("`RGB() function does not set special values (border values"
@ "`0`, `255` and value `10`, incorrect for coloring a `string`) for"
@"red, green and blue components correctly.");
createdColor = _().color.RGB(0, 10, 255);
createdColor = __().color.RGB(0, 10, 255);
TEST_ExpectTrue(createdColor.r == 0);
TEST_ExpectTrue(createdColor.g == 10);
TEST_ExpectTrue(createdColor.b == 255);
@ -67,7 +67,7 @@ protected static function SubTest_ColorCreationRGBA()
local Color createdColor;
Issue("`RGBA() function does not set red, green, blue, alpha"
@ "components correctly.");
createdColor = _().color.RGBA(93, 245, 1, 67);
createdColor = __().color.RGBA(93, 245, 1, 67);
TEST_ExpectTrue(createdColor.r == 93);
TEST_ExpectTrue(createdColor.g == 245);
TEST_ExpectTrue(createdColor.b == 1);
@ -76,7 +76,7 @@ protected static function SubTest_ColorCreationRGBA()
Issue("`RGBA() function does not set special values (border values"
@ "`0`, `255` and value `10`, incorrect for coloring a `string`) for"
@"red, green, blue components correctly.");
createdColor = _().color.RGBA(0, 10, 10, 255);
createdColor = __().color.RGBA(0, 10, 10, 255);
TEST_ExpectTrue(createdColor.r == 0);
TEST_ExpectTrue(createdColor.g == 10);
TEST_ExpectTrue(createdColor.b == 10);
@ -93,91 +93,91 @@ protected static function Test_EqualityCheck()
protected static function SubTest_EqualityCheckNotFixed()
{
local Color color1, color2, color3;
color1 = _().color.RGB(45, 10, 19);
color2 = _().color.RGB(45, 11, 19);
color3 = _().color.RGBA(45, 10, 19, 178);
color1 = __().color.RGB(45, 10, 19);
color2 = __().color.RGB(45, 11, 19);
color3 = __().color.RGBA(45, 10, 19, 178);
Issue("`AreEqual()` does not recognized equal colors as such.");
TEST_ExpectTrue(_().color.AreEqual(color1, color1));
TEST_ExpectTrue(__().color.AreEqual(color1, color1));
Issue("`AreEqual()` does not recognized colors that differ only in alpha"
@ "channel as equal.");
TEST_ExpectTrue(_().color.AreEqual(color1, color3));
TEST_ExpectTrue(__().color.AreEqual(color1, color3));
Issue("`AreEqual()` does not recognized different colors as such.");
TEST_ExpectFalse(_().color.AreEqual(color1, color2));
TEST_ExpectFalse(__().color.AreEqual(color1, color2));
Issue("`AreEqualWithAlpha()` does not recognized equal colors as such.");
TEST_ExpectTrue(_().color.AreEqualWithAlpha(color1, color1));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(color3, color3));
TEST_ExpectTrue(__().color.AreEqualWithAlpha(color1, color1));
TEST_ExpectTrue(__().color.AreEqualWithAlpha(color3, color3));
Issue("`AreEqualWithAlpha()` does not recognized different colors"
@ "as such.");
TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color2));
TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color3));
TEST_ExpectFalse(__().color.AreEqualWithAlpha(color1, color2));
TEST_ExpectFalse(__().color.AreEqualWithAlpha(color1, color3));
}
protected static function SubTest_EqualityCheckFixed()
{
local Color color1, color2, color3;
color1 = _().color.RGB(45, 10, 0);
color2 = _().color.RGB(45, 239, 19);
color3 = _().color.RGBA(45, 11, 1, 178);
color1 = __().color.RGB(45, 10, 0);
color2 = __().color.RGB(45, 239, 19);
color3 = __().color.RGBA(45, 11, 1, 178);
Issue("`AreEqual()` does not recognized equal colors as such (with color"
@ "auto-fix).");
TEST_ExpectTrue(_().color.AreEqual(color1, color1, true));
TEST_ExpectTrue(__().color.AreEqual(color1, color1, true));
Issue("`AreEqual()` does not recognized colors that differ only in alpha"
@ "channel as equal (with color auto-fix).");
TEST_ExpectTrue(_().color.AreEqual(color1, color3, true));
TEST_ExpectTrue(__().color.AreEqual(color1, color3, true));
Issue("`AreEqual()` does not recognized different colors as such"
@ "(with color auto-fix).");
TEST_ExpectFalse(_().color.AreEqual(color1, color2, true));
TEST_ExpectFalse(__().color.AreEqual(color1, color2, true));
Issue("`AreEqualWithAlpha()` does not recognized equal colors as such"
@ "(with color auto-fix).");
TEST_ExpectTrue(_().color.AreEqualWithAlpha(color1, color1, true));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(color3, color3, true));
TEST_ExpectTrue(__().color.AreEqualWithAlpha(color1, color1, true));
TEST_ExpectTrue(__().color.AreEqualWithAlpha(color3, color3, true));
Issue("`AreEqualWithAlpha()` does not recognized different colors as such"
@ "(with color auto-fix).");
TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color2, true));
TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color3, true));
TEST_ExpectFalse(__().color.AreEqualWithAlpha(color1, color2, true));
TEST_ExpectFalse(__().color.AreEqualWithAlpha(color1, color3, true));
}
protected static function Test_ColorFixing()
{
local Color validColor, brokenColor;
validColor = _().color.RGB(23, 179, 244);
brokenColor = _().color.RGB(10, 35, 0);
validColor = __().color.RGB(23, 179, 244);
brokenColor = __().color.RGB(10, 35, 0);
Context("Testing `ColorAPI`'s functions for fixing color components for"
@ "game's native render functions.");
Issue("`FixColorComponent()` does not \"fix\" values it is expected to,"
@ "the way it is expected to.");
TEST_ExpectTrue(_().color.FixColorComponent(0) == 1);
TEST_ExpectTrue(_().color.FixColorComponent(10) == 11);
TEST_ExpectTrue(__().color.FixColorComponent(0) == 1);
TEST_ExpectTrue(__().color.FixColorComponent(10) == 11);
Issue("`FixColorComponent()` changes values it should not.");
TEST_ExpectTrue(_().color.FixColorComponent(9) == 9);
TEST_ExpectTrue(_().color.FixColorComponent(255) == 255);
TEST_ExpectTrue(_().color.FixColorComponent(87) == 87);
TEST_ExpectTrue(__().color.FixColorComponent(9) == 9);
TEST_ExpectTrue(__().color.FixColorComponent(255) == 255);
TEST_ExpectTrue(__().color.FixColorComponent(87) == 87);
Issue("`FixColor()` changes colors it should not.");
TEST_ExpectTrue(
_().color.AreEqualWithAlpha(validColor,
_().color.FixColor(validColor)));
__().color.AreEqualWithAlpha(validColor,
__().color.FixColor(validColor)));
Issue("`FixColor()` doesn't fix color it should fix in an expected way.");
TEST_ExpectTrue(
_().color.AreEqualWithAlpha(_().color.RGB(11, 35, 1),
_().color.FixColor(brokenColor)));
__().color.AreEqualWithAlpha(__().color.RGB(11, 35, 1),
__().color.FixColor(brokenColor)));
Issue("`FixColor()` affects alpha channel.");
TEST_ExpectTrue(_().color.FixColor(validColor).a == 255);
TEST_ExpectTrue(__().color.FixColor(validColor).a == 255);
validColor.a = 0;
TEST_ExpectTrue(_().color.FixColor(validColor).a == 0);
TEST_ExpectTrue(__().color.FixColor(validColor).a == 0);
validColor.a = 10;
TEST_ExpectTrue(_().color.FixColor(validColor).a == 10);
TEST_ExpectTrue(__().color.FixColor(validColor).a == 10);
}
protected static function Test_ToString()
@ -190,49 +190,50 @@ protected static function Test_ToString()
protected static function SubTest_ToStringType()
{
local Color normalColor, borderValueColor;
normalColor = _().color.RGBA(24, 232, 187, 34);
borderValueColor = _().color.RGBA(0, 255, 255, 0);
normalColor = __().color.RGBA(24, 232, 187, 34);
borderValueColor = __().color.RGBA(0, 255, 255, 0);
Issue("`ToStringType()` improperly works with `CLRDISPLAY_HEX` option.");
TEST_ExpectTrue(_().color.ToStringType(normalColor) ~= "#18e8bb");
TEST_ExpectTrue(_().color.ToStringType(borderValueColor) ~= "#00ffff");
TEST_ExpectTrue(__().color.ToStringType(normalColor) ~= "#18e8bb");
TEST_ExpectTrue(__().color.ToStringType(borderValueColor) ~= "#00ffff");
Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGB` option.");
TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGB)
TEST_ExpectTrue(__().color.ToStringType(normalColor, CLRDISPLAY_RGB)
~= "rgb(24,232,187)");
TEST_ExpectTrue(_().color.ToStringType(borderValueColor, CLRDISPLAY_RGB)
TEST_ExpectTrue(__().color.ToStringType(borderValueColor, CLRDISPLAY_RGB)
~= "rgb(0,255,255)");
Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGBA` option.");
TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGBA)
TEST_ExpectTrue(__().color.ToStringType(normalColor, CLRDISPLAY_RGBA)
~= "rgba(24,232,187,34)");
TEST_ExpectTrue(_().color.ToStringType(borderValueColor, CLRDISPLAY_RGBA)
TEST_ExpectTrue(__().color.ToStringType(borderValueColor, CLRDISPLAY_RGBA)
~= "rgba(0,255,255,0)");
Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGB_TAG`"
@ "option.");
TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGB_TAG)
TEST_ExpectTrue(__().color.ToStringType(normalColor, CLRDISPLAY_RGB_TAG)
~= "rgb(r=24,g=232,b=187)");
TEST_ExpectTrue(_().color.ToStringType(borderValueColor, CLRDISPLAY_RGB_TAG)
TEST_ExpectTrue(
__().color.ToStringType(borderValueColor, CLRDISPLAY_RGB_TAG)
~= "rgb(r=0,g=255,b=255)");
Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGBA_TAG`"
@ "option.");
TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGBA_TAG)
TEST_ExpectTrue(__().color.ToStringType(normalColor, CLRDISPLAY_RGBA_TAG)
~= "rgba(r=24,g=232,b=187,a=34)");
TEST_ExpectTrue(
_().color.ToStringType(borderValueColor, CLRDISPLAY_RGBA_TAG)
__().color.ToStringType(borderValueColor, CLRDISPLAY_RGBA_TAG)
~= "rgba(r=0,g=255,b=255,a=0)");
}
protected static function SubTest_ToString()
{
local Color opaqueColor, transparentColor;
opaqueColor = _().color.RGBA(143, 211, 43, 255);
transparentColor = _().color.RGBA(234, 32, 145, 13);
opaqueColor = __().color.RGBA(143, 211, 43, 255);
transparentColor = __().color.RGBA(234, 32, 145, 13);
Issue("`ToString()` improperly converts color with opaque color.");
TEST_ExpectTrue(_().color.ToString(opaqueColor) ~= "rgb(143,211,43)");
TEST_ExpectTrue(__().color.ToString(opaqueColor) ~= "rgb(143,211,43)");
Issue("`ToString()` improperly converts color with transparent color.");
TEST_ExpectTrue(_().color.ToString(transparentColor)
TEST_ExpectTrue(__().color.ToString(transparentColor)
~= "rgba(234,32,145,13)");
}
@ -247,35 +248,35 @@ protected static function Test_GetTag()
protected static function SubTest_GetTagColor()
{
local Color normalColor, borderColor;
normalColor = _().color.RGB(143, 211, 43);
borderColor = _().color.RGB(10, 0, 255);
normalColor = __().color.RGB(143, 211, 43);
borderColor = __().color.RGB(10, 0, 255);
Issue("`GetColorTag()` does not properly convert colors.");
TEST_ExpectTrue(_().color.GetColorTag(normalColor)
TEST_ExpectTrue(__().color.GetColorTag(normalColor)
== (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43)));
TEST_ExpectTrue(_().color.GetColorTag(borderColor)
TEST_ExpectTrue(__().color.GetColorTag(borderColor)
== (Chr(27) $ Chr(11) $ Chr(1) $ Chr(255)));
Issue("`GetColorTag()` does not properly convert colors when asked not to"
@ "fix components.");
TEST_ExpectTrue(_().color.GetColorTag(normalColor, true)
TEST_ExpectTrue(__().color.GetColorTag(normalColor, true)
== (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43)));
TEST_ExpectTrue(_().color.GetColorTag(borderColor, true)
TEST_ExpectTrue(__().color.GetColorTag(borderColor, true)
== (Chr(27) $ Chr(10) $ Chr(1) $ Chr(255)));
}
protected static function SubTest_GetTagRGB()
{
Issue("`GetColorTagRGB()` does not properly convert colors.");
TEST_ExpectTrue(_().color.GetColorTagRGB(143, 211, 43)
TEST_ExpectTrue(__().color.GetColorTagRGB(143, 211, 43)
== (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43)));
TEST_ExpectTrue(_().color.GetColorTagRGB(10, 0, 255)
TEST_ExpectTrue(__().color.GetColorTagRGB(10, 0, 255)
== (Chr(27) $ Chr(11) $ Chr(1) $ Chr(255)));
Issue("`GetColorTagRGB()` does not properly convert colors when asked"
@ "not to fix components.");
TEST_ExpectTrue(_().color.GetColorTagRGB(143, 211, 43, true)
TEST_ExpectTrue(__().color.GetColorTagRGB(143, 211, 43, true)
== (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43)));
TEST_ExpectTrue(_().color.GetColorTagRGB(10, 0, 255, true)
TEST_ExpectTrue(__().color.GetColorTagRGB(10, 0, 255, true)
== (Chr(27) $ Chr(10) $ Chr(1) $ Chr(255)));
}
@ -284,224 +285,115 @@ protected static function Test_Parse()
Context("Testing `ColorAPI`'s parsing functionality.");
SubTest_ParseWithParser();
SubTest_ParseStringPlain();
SubTest_ParseStringColored();
SubTest_ParseStringFormatted();
SubTest_ParseText();
SubTest_ParseRaw();
}
protected static function SubTest_ParseWithParser()
{
local Color expectedColor, resultColor;
expectedColor = _().color.RGBA(154, 255, 0, 187);
expectedColor = __().color.RGBA(154, 255, 0, 187);
Issue("`ParseWith()` cannot parse hex colors.");
TEST_ExpectTrue(_().color.ParseWith(_().text.ParseString("#9aff00"),
TEST_ExpectTrue(__().color.ParseWith(__().text.ParseString("#9aff00"),
resultColor));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
TEST_ExpectTrue(__().color.AreEqual(resultColor, expectedColor));
Issue("`ParseWith()` cannot parse rgb colors.");
TEST_ExpectTrue(_().color.ParseWith(_().text.ParseString("rgb(154,255,0)"),
TEST_ExpectTrue(__().color.ParseWith(__().text.ParseString("rgb(154,255,0)"),
resultColor));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
TEST_ExpectTrue(__().color.AreEqual(resultColor, expectedColor));
Issue("`ParseWith()` cannot parse rgba colors.");
TEST_ExpectTrue(_().color.ParseWith(
_().text.ParseString("rgba(154,255,0,187)"),
TEST_ExpectTrue(__().color.ParseWith(
__().text.ParseString("rgba(154,255,0,187)"),
resultColor));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
TEST_ExpectTrue(__().color.AreEqualWithAlpha(resultColor, expectedColor));
Issue("`ParseWith()` cannot parse rgb colors with tags.");
TEST_ExpectTrue(_().color.ParseWith(
_().text.ParseString("rgb(r=154,g=255,b=0)"),
TEST_ExpectTrue(__().color.ParseWith(
__().text.ParseString("rgb(r=154,g=255,b=0)"),
resultColor));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
TEST_ExpectTrue(__().color.AreEqual(resultColor, expectedColor));
Issue("`ParseWith()` cannot parse rgba colors with tags.");
TEST_ExpectTrue(_().color.ParseWith(
_().text.ParseString("rgba(r=154,g=255,b=0,a=187)"),
TEST_ExpectTrue(__().color.ParseWith(
__().text.ParseString("rgba(r=154,g=255,b=0,a=187)"),
resultColor));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
TEST_ExpectTrue(__().color.AreEqualWithAlpha(resultColor, expectedColor));
Issue("`ParseWith()` reports success when parsing invalid color string.");
TEST_ExpectFalse(_().color.ParseWith( _().text.ParseString("#9aff0g"),
TEST_ExpectFalse(__().color.ParseWith( __().text.ParseString("#9aff0g"),
resultColor));
}
protected static function SubTest_ParseStringPlain()
{
local Color expectedColor, resultColor;
expectedColor = _().color.RGBA(154, 255, 0, 187);
expectedColor = __().color.RGBA(154, 255, 0, 187);
Issue("`ParseString()` cannot parse hex colors.");
TEST_ExpectTrue(_().color.ParseString("#9aff00", resultColor));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
TEST_ExpectTrue(__().color.ParseString("#9aff00", resultColor));
TEST_ExpectTrue(__().color.AreEqual(resultColor, expectedColor));
Issue("`ParseString()` cannot parse rgb colors.");
TEST_ExpectTrue(_().color.ParseString("rgb(154,255,0)", resultColor));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
TEST_ExpectTrue(__().color.ParseString("rgb(154,255,0)", resultColor));
TEST_ExpectTrue(__().color.AreEqual(resultColor, expectedColor));
Issue("`ParseString()` cannot parse rgba colors.");
TEST_ExpectTrue(_().color.ParseString("rgba(154,255,0,187)", resultColor));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
TEST_ExpectTrue(__().color.ParseString("rgba(154,255,0,187)", resultColor));
TEST_ExpectTrue(__().color.AreEqualWithAlpha(resultColor, expectedColor));
Issue("`ParseString()` cannot parse rgb colors with tags.");
TEST_ExpectTrue(_().color.ParseString("rgb(r=154,g=255,b=0)", resultColor));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
TEST_ExpectTrue(__().color.ParseString("rgb(r=154,g=255,b=0)",
resultColor));
TEST_ExpectTrue(__().color.AreEqual(resultColor, expectedColor));
Issue("`ParseString()` cannot parse rgba colors with tags.");
TEST_ExpectTrue(_().color.ParseString( "rgba(r=154,g=255,b=0,a=187)",
TEST_ExpectTrue(__().color.ParseString( "rgba(r=154,g=255,b=0,a=187)",
resultColor));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
TEST_ExpectTrue(__().color.AreEqualWithAlpha(resultColor, expectedColor));
Issue("`ParseString()` reports success when parsing invalid color string.");
TEST_ExpectFalse(_().color.ParseString("#9aff0g", resultColor));
}
protected static function SubTest_ParseStringColored()
{
local Color expectedColor, resultColor;
expectedColor = _().color.RGBA(154, 255, 0, 187);
Issue("`ParseString(STRING_Colored)` cannot parse hex colors.");
TEST_ExpectTrue(_().color.ParseString(
"#9af" $ Chr(27) $ Chr(45) $ Chr(234) $ Chr(24) $ "f00",
resultColor, STRING_Colored));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
Issue("`ParseString(STRING_Colored)` cannot parse rgb colors.");
TEST_ExpectTrue(_().color.ParseString(
"rgb(154,2" $ Chr(27) $ Chr(23) $ Chr(32) $ Chr(53) $ "55,0)",
resultColor, STRING_Colored));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
Issue("`ParseString(STRING_Colored)` cannot parse rgba colors.");
TEST_ExpectTrue(_().color.ParseString(
"rgba(154,255,0,187" $ Chr(27) $ Chr(133) $ Chr(234) $ Chr(10) $ ")",
resultColor, STRING_Colored));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
Issue("`ParseString(STRING_Colored)` cannot parse rgb colors with tags.");
TEST_ExpectTrue(_().color.ParseString(
"rg" $ Chr(27) $ Chr(26) $ Chr(234) $ Chr(125) $ "b(r=154,g=255,b=0)",
resultColor, STRING_Colored));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
Issue("`ParseString(STRING_Colored)` cannot parse rgba colors with tags.");
TEST_ExpectTrue(_().color.ParseString(
"rgba(r=154,g=255,b" $ Chr(27) $ Chr(1) $ Chr(4) $ Chr(7) $ "=0,a=187)",
resultColor, STRING_Colored));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
}
protected static function SubTest_ParseStringFormatted()
{
local Color expectedColor, resultColor;
expectedColor = _().color.RGBA(154, 255, 0, 187);
Issue("`ParseString(STRING_Formatted)` cannot parse hex colors.");
TEST_ExpectTrue(_().color.ParseString(
"#9a{#4753d5 ff0}0",
resultColor, STRING_Formatted));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
Issue("`ParseString(STRING_Formatted)` cannot parse rgb colors.");
TEST_ExpectTrue(_().color.ParseString(
"rg{rgb(45,67,123) b(154,25}5,0)",
resultColor, STRING_Formatted));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
Issue("`ParseString(STRING_Formatted)` cannot parse rgba colors.");
TEST_ExpectTrue(_().color.ParseString(
"rgba(154,2{#34d1a7 }55,0,187)",
resultColor, STRING_Formatted));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
Issue("`ParseString(STRING_Formatted)` cannot parse rgb colors with tags.");
TEST_ExpectTrue(_().color.ParseString(
"rgb(r{#34d1a7 }=154,g=255,b=0)",
resultColor, STRING_Formatted));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
Issue("`ParseString(STRING_Formatted)` cannot parse rgba colors with"
@ "tags.");
TEST_ExpectTrue(_().color.ParseString(
"r{rgb(12,12,253) gba(r=154,g=255,b=0,a=187)}",
resultColor, STRING_Formatted));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
TEST_ExpectFalse(__().color.ParseString("#9aff0g", resultColor));
}
protected static function SubTest_ParseText()
{
local Color expectedColor, resultColor;
expectedColor = _().color.RGBA(154, 255, 0, 187);
expectedColor = __().color.RGBA(154, 255, 0, 187);
Issue("`Parse()` cannot parse hex colors.");
TEST_ExpectTrue(_().color.Parse(_().text.FromString("#9aff00"),
TEST_ExpectTrue(__().color.Parse(__().text.FromString("#9aff00"),
resultColor));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
TEST_ExpectTrue(__().color.AreEqual(resultColor, expectedColor));
Issue("`Parse()` cannot parse rgb colors.");
TEST_ExpectTrue(_().color.Parse(_().text.FromString("rgb(154,255,0)"),
TEST_ExpectTrue(__().color.Parse(__().text.FromString("rgb(154,255,0)"),
resultColor));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
TEST_ExpectTrue(__().color.AreEqual(resultColor, expectedColor));
Issue("`Parse()` cannot parse rgba colors.");
TEST_ExpectTrue(_().color.Parse(
_().text.FromString("rgba(154,255,0,187)"),
TEST_ExpectTrue(__().color.Parse(
__().text.FromString("rgba(154,255,0,187)"),
resultColor));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
TEST_ExpectTrue(__().color.AreEqualWithAlpha(resultColor, expectedColor));
Issue("`Parse()` cannot parse rgb colors with tags.");
TEST_ExpectTrue(_().color.Parse(
_().text.FromString("rgb(r=154,g=255,b=0)"),
TEST_ExpectTrue(__().color.Parse(
__().text.FromString("rgb(r=154,g=255,b=0)"),
resultColor));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
TEST_ExpectTrue(__().color.AreEqual(resultColor, expectedColor));
Issue("`Parse()` cannot parse rgba colors with tags.");
TEST_ExpectTrue(_().color.Parse(
_().text.FromString("rgba(r=154,g=255,b=0,a=187)"),
TEST_ExpectTrue(__().color.Parse(
__().text.FromString("rgba(r=154,g=255,b=0,a=187)"),
resultColor));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
TEST_ExpectTrue(__().color.AreEqualWithAlpha(resultColor, expectedColor));
Issue("`Parse()` reports success when parsing invalid color string.");
TEST_ExpectFalse(_().color.Parse( _().text.FromString("#9aff0g"),
resultColor));
}
protected static function SubTest_ParseRaw()
{
local Color expectedColor, resultColor;
expectedColor = _().color.RGBA(154, 255, 0, 187);
Issue("`ParseRaw()` cannot parse hex colors.");
TEST_ExpectTrue(_().color.ParseRaw( _().text.StringToRaw("#9aff00"),
resultColor));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
Issue("`ParseRaw()` cannot parse rgb colors.");
TEST_ExpectTrue(_().color.ParseRaw( _().text.StringToRaw("rgb(154,255,0)"),
resultColor));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
Issue("`ParseRaw()` cannot parse rgba colors.");
TEST_ExpectTrue(_().color.ParseRaw(
_().text.StringToRaw("rgba(154,255,0,187)"),
resultColor));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
Issue("`ParseRaw()` cannot parse rgb colors with tags.");
TEST_ExpectTrue(_().color.ParseRaw(
_().text.StringToRaw("rgb(r=154,g=255,b=0)"),
resultColor));
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
Issue("`ParseRaw()` cannot parse rgba colors with tags.");
TEST_ExpectTrue(_().color.ParseRaw(
_().text.StringToRaw("rgba(r=154,g=255,b=0,a=187)"),
resultColor));
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
Issue("`ParseRaw()` reports success when parsing invalid color string.");
TEST_ExpectFalse(_().color.ParseRaw(_().text.StringToRaw("#9aff0g"),
TEST_ExpectFalse(__().color.Parse( __().text.FromString("#9aff0g"),
resultColor));
}
defaultproperties
{
caseName = "Colors"
caseName = "ColorAPI"
caseGroup = "Color"
}

25
sources/Commands/Aliases/CommandAliasSource.uc

@ -0,0 +1,25 @@
/**
* Source intended for AcediaCore's command aliases.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandAliasSource extends AliasSource;
defaultproperties
{
aliasesClass = class'CommandAliases'
}

26
sources/Commands/Aliases/CommandAliases.uc

@ -0,0 +1,26 @@
/**
* Per-object-configuration intended for AcediaCore's command aliases.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandAliases extends Aliases
perObjectConfig;
defaultproperties
{
sourceClass = class'CommandAliasSource'
}

59
sources/Commands/BroadcastListener_Commands.uc

@ -0,0 +1,59 @@
/**
* Overloaded broadcast events listener to catch commands input from
* the in-game chat.
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class BroadcastListener_Commands extends BroadcastListenerBase
abstract;
// TODO: reimplement with even to provide `APlayer` in the first place
static function bool HandleText(
Actor sender,
out string message,
optional name messageType)
{
local APlayer callerPlayer;
local Parser parser;
local Commands commandFeature;
local PlayerService service;
// We only want to catch chat messages
// and only if `Commands` feature is active
if (messageType != 'Say') return true;
commandFeature = Commands(class'Commands'.static.GetInstance());
if (commandFeature == none) return true;
if (!commandFeature.useChatInput) return true;
// We are only interested in messages that start with "!"
parser = __().text.ParseString(message);
if (!parser.Match(P("!")).Ok()) {
parser.FreeSelf();
return true;
}
// Extract `APlayer` from the `sender`
service = PlayerService(class'PlayerService'.static.Require());
if (service != none) {
callerPlayer = service.GetPlayer(PlayerController(sender));
}
// Pass input to command feature
commandFeature.HandleInput(parser, callerPlayer);
parser.FreeSelf();
return false;
}
defaultproperties
{
}

103
sources/Commands/BuiltInCommands/ACommandDosh.uc

@ -0,0 +1,103 @@
/**
* Command for changing amount of money players have.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandDosh extends Command;
//'dosh' for giving dosh (subcommand for setting it, options for min/max resulting value, silent)
protected function BuildData(CommandDataBuilder builder)
{
builder.RequireTarget();
builder.ParamInteger(P("amount"))
.Describe(P("Gives (takes if negative) players a specified <amount>"
@ "of money."));
builder.SubCommand(P("set"))
.ParamIntegerList(P("amount"))
.Describe(P("Sets players' money to a specified <amount>."));
builder.Option(P("silent"))
.Describe(P("If specified - players won't receive a notification about"
@ "obtaining/losing dosh."));
builder.Option(P("min"))
.ParamInteger(P("minValue"))
.Describe(F("Players will retain at least this amount of dosh after"
@ "the command's execution. In case of conflict overrides"
@ "'{$TextEmphasis --max}' option. `0` is assumed by default."));
builder.Option(P("max"), P("M"))
.ParamInteger(P("maxValue"))
.Describe(F("Players will have at most this amount of dosh after"
@ "the command's execution. In case of conflict is overridden by"
@ "'{$TextEmphasis --min}' option."));
}
protected function ExecutedFor(APlayer player, CommandCall result)
{
local int oldAmount, newAmount;
local int amount, minValue, maxValue;
local AssociativeArray commandOptions;
// Find min and max value boundaries
minValue = 0;
maxValue = MaxInt;
commandOptions = result.GetOptions();
if (commandOptions.HasKey(P("min")))
{
minValue = IntBox(AssociativeArray(commandOptions.GetItem(P("min")))
.GetItem(P("minValue"))).Get();
}
if (commandOptions.HasKey(P("max"))) {
minValue = IntBox(AssociativeArray(commandOptions.GetItem(P("max")))
.GetItem(P("maxValue"))).Get();
}
if (minValue > maxValue) {
maxValue = minValue;
}
// Change dosh
oldAmount = player.GetDosh();
amount = IntBox(result.GetParameters().GetItem(P("amount"))).Get();
if (result.GetSubCommand().IsEmpty()) {
newAmount = oldAmount + amount;
}
else {
newAmount = amount;
}
// Enforce min/max bounds
if (newAmount > maxValue) {
newAmount = maxValue;
}
if (newAmount < minValue) {
newAmount = minValue;
}
if (!commandOptions.HasKey(P("silent")))
{
if (newAmount > oldAmount)
{
player.Console().WriteLine(P("You've gotten"
@ newAmount - oldAmount @ "dosh!"));
}
if (newAmount < oldAmount)
{
player.Console().WriteLine(P("You've lost"
@ oldAmount - newAmount @ "dosh!"));
}
}
player.SetDosh(newAmount);
}
defaultproperties
{
commandName = "dosh"
}

101
sources/Commands/BuiltInCommands/ACommandHelp.uc

@ -0,0 +1,101 @@
/**
* Command for displaying help information about registered Acedia's commands.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandHelp extends Command;
protected function BuildData(CommandDataBuilder builder)
{
builder.OptionalParams()
.ParamTextList(P("commands"))
.Describe(P("Displays information about all specified commands."));
builder.Option(P("list"))
.Describe(P("Displays list of all available commands."));
}
protected function Executed(CommandCall callInfo)
{
local AssociativeArray parameters;
local DynamicArray commandsToDisplay;
local APlayer callerPlayer;
callerPlayer = callInfo.GetCallerPlayer();
if (callerPlayer == none) return;
// Command list
if (callInfo.GetOptions().HasKey(P("list"))) {
DisplayCommandList(callerPlayer);
}
// Help pages
parameters = callInfo.GetParameters();
commandsToDisplay = DynamicArray(parameters.GetItem(P("commands")));
DisplayCommandHelpPages(callerPlayer, commandsToDisplay);
}
private final function DisplayCommandList(APlayer player)
{
local int i;
local ConsoleWriter console;
local array<Text> commandNames;
local Commands commandsFeature;
if (player == none) return;
commandsFeature = Commands(class'Commands'.static.GetInstance());
if (commandsFeature == none) return;
console = player.Console();
commandNames = commandsFeature.GetCommandNames();
for (i = 0; i < commandNames.length; i += 1) {
console.WriteLine(commandNames[i]);
}
_.memory.FreeMany(commandNames);
}
private final function DisplayCommandHelpPages(
APlayer player,
DynamicArray commandList)
{
local int i;
local Text nextHelpPage;
local Command nextCommand;
local Commands commandsFeature;
if (player == none) return;
commandsFeature = Commands(class'Commands'.static.GetInstance());
if (commandsFeature == none) return;
// If arguments were empty - at least display our own help page
if (commandList.GetLength() == 1 && Text(commandList.GetItem(0)).IsEmpty())
{
nextHelpPage = PrintHelp();
player.Console().WriteLine(nextHelpPage).Flush();
nextHelpPage.FreeSelf();
return;
}
for (i = 0; i < commandList.GetLength(); i += 1)
{
nextCommand = commandsFeature.GetCommand(Text(commandList.GetItem(i)));
if (nextCommand == none) continue;
nextHelpPage = nextCommand.PrintHelp();
player.Console().WriteLine(nextHelpPage);
nextHelpPage.FreeSelf();
}
player.Console().Flush();
}
defaultproperties
{
commandName = "help"
}

38
sources/Commands/BuiltInCommands/ACommandNick.uc

@ -0,0 +1,38 @@
/**
* Command for changing nickname of the player.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandNick extends Command;
//'dosh' for giving dosh (subcommand for setting it, options for min/max resulting value, silent)
protected function BuildData(CommandDataBuilder builder)
{
builder.RequireTarget();
builder.ParamText(P("nick"))
.Describe(P("Sets new nickname to the targeted players."));
}
protected function ExecutedFor(APlayer player, CommandCall result)
{
player.SetName(Text(result.GetParameters().GetItem(P("nick"))));
}
defaultproperties
{
commandName = "nick"
}

607
sources/Commands/Command.uc

@ -0,0 +1,607 @@
/**
* This class is meant to represent a command type: to create new command
* one should extend it, then simply define required sub-commands/options and
* parameters in `BuildData()` and use `Execute()` / `ExecuteFor()` to perform
* necessary actions when command is executed by a player.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Command extends AcediaObject
dependson(Text);
/**
* Possible errors that can arise when producing `CommandCall` from user input
*/
enum ErrorType
{
// No error
CET_None,
// Bad parser was provided to parse user input
// (this should not be possible)
CET_BadParser,
// Sub-command name was not specified or was incorrect
// (this should not be possible)
CET_NoSubCommands,
// Required param for command / option was not specified
CET_NoRequiredParam,
CET_NoRequiredParamForOption,
// Unknown option key was specified
CET_UnknownOption,
CET_UnknownShortOption,
// Same option appeared twice in one command call
CET_RepeatedOption,
// Part of user's input could not be interpreted as a part of
// command's call
CET_UnusedCommandParameters,
// In one short option specification (e.g. '-lah') several options
// require parameters: this introduces ambiguity and is not allowed
CET_MultipleOptionsWithParams,
// (For targeted commands only)
// Targets are specified incorrectly (or none actually specified)
CET_IncorrectTargetList,
CET_EmptyTargetList
};
/**
* Possible types of parameters.
*/
enum ParameterType
{
CPT_Boolean,
CPT_Integer,
CPT_Number,
CPT_Text,
CPT_Object,
CPT_Array
};
/**
* Possible forms a boolean variable can be used as.
* Boolean parameter can define it's preferred format, which will be used
* for help page generation.
*/
enum PreferredBooleanFormat
{
PBF_TrueFalse,
PBF_EnableDisable,
PBF_OnOff,
PBF_YesNo
};
// Defines a singular command parameter
struct Parameter
{
// Display name (for the needs of help page displaying)
var Text displayName;
// Type of value this parameter would store
var ParameterType type;
// Does it take only a singular value or can it contain several of them,
// written in a list
var bool allowsList;
// Variable name that will be used as a key to store parameter's value
var Text variableName;
// (For `CPT_Boolean` type variables only) - preferred boolean format,
// used in help pages
var PreferredBooleanFormat booleanFormat;
};
// Defines a sub-command of a this command (specified as
// "<command> <sub_command>").
// Using sub-command is not optional, but if none defined
// (in `BuildData()`) / specified by the player - an empty (`name.IsEmpty()`)
// one is automatically created / used.
struct SubCommand
{
// Cannot be `none`
var Text name;
// Can be `none`
var Text description;
var array<Parameter> required;
var array<Parameter> optional;
};
// Defines command's option (options are specified by "--long" or "-l").
// Options are independent from sub-commands.
struct Option
{
var Text.Character shortName;
var Text longName;
var Text description;
// Option can also have their own parameters
var array<Parameter> required;
var array<Parameter> optional;
};
// Structure that defines what sub-commands and options command has
// (and what parameters they take)
struct Data
{
var protected array<SubCommand> subCommands;
var protected array<Option> options;
var protected bool requiresTarget;
};
var private Data commandData;
// Default command name that will be used unless Acedia is configured to
// do otherwise
var private const string commandName;
// We do not really ever need to create more than one instance of each class
// of `Command`, so we will simply store and reuse one created instance.
var private Command mainInstance;
var public const int TSPACE, TCOMMAND_NAME_FALLBACK, TPLUS;
var public const int TOPEN_BRACKET, TCLOSE_BRACKET;
var public const int TKEY, TDOUBLE_KEY, TCOMMA_SPACE, TBOOLEAN, TINDENT;
var public const int TBOOLEAN_TRUE_FALSE, TBOOLEAN_ENABLE_DISABLE;
var public const int TBOOLEAN_ON_OFF, TBOOLEAN_YES_NO;
var public const int TOPTIONS, TCMD_WITH_TARGET, TCMD_WITHOUT_TARGET;
protected function Constructor()
{
local CommandDataBuilder dataBuilder;
dataBuilder =
CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
BuildData(dataBuilder);
commandData = dataBuilder.GetData();
dataBuilder.FreeSelf();
dataBuilder = none;
}
/**
* Overload this method to use `builder` to define parameters and options for
* your command.
*
* @param builder Builder that can be used to define your commands parameters
* and options. Do not deallocate.
*/
protected function BuildData(CommandDataBuilder builder){}
/**
* Overload this method to perform what is needed when your command is called.
*
* @param callInfo Object filled with parameters that your command has
* been called with. Guaranteed to not be in error state.
*/
protected function Executed(CommandCall callInfo){}
/**
* Overload this method to perform what is needed when your command is called
* with a given player as a target. If several players have been specified -
* this method will be called once for each.
*
* If your command does not require a target - this method will not be called.
*
* @param targetPlayer Player that this command must perform an action on.
* @param callInfo Object filled with parameters that your command has
* been called with. Guaranteed to not be in error state.
*/
protected function ExecutedFor(APlayer targetPlayer, CommandCall callInfo){}
/**
* Returns an instance of command (of particular class) that is stored
* "as a singleton" in command's class itself. Do not deallocate it.
*/
public final static function Command GetInstance()
{
if (default.mainInstance == none) {
default.mainInstance = Command(__().memory.Allocate(default.class));
}
return default.mainInstance;
}
/**
* Returns name (in lower case) of the caller command class.
*
* @return Name (in lower case) of the caller command class.
*/
public final static function Text GetName()
{
local Text name, lowerCaseName;
name = __().text.FromString(default.commandName);
lowerCaseName = name.LowerCopy();
name.FreeSelf();
return lowerCaseName;
}
/**
* Forces command to process (parse and, if successful, execute itself)
* player's input.
*
* @param parser Parser that contains player's input.
* @param callerPlayer Player that initiated this command's call.
* @return `CommandCall` object that described parsed command call.
* Guaranteed to be not `none`.
*/
public final function CommandCall ProcessInput(
Parser parser,
APlayer callerPlayer)
{
local int i;
local array<APlayer> targetPlayers;
local CommandParser commandParser;
local CommandCall callInfo;
if (parser == none || !parser.Ok()) {
return MakeAndReportError(callerPlayer, CET_BadParser);
}
// Parse targets and handle errors that can arise here
if (commandData.requiresTarget)
{
targetPlayers = ParseTargets(parser, callerPlayer);
if (!parser.Ok()) {
return MakeAndReportError(callerPlayer, CET_IncorrectTargetList);
}
if (targetPlayers.length <= 0) {
return MakeAndReportError(callerPlayer, CET_EmptyTargetList);
}
}
// Parse parameters themselves
commandParser = CommandParser(_.memory.Allocate(class'CommandParser'));
callInfo = commandParser.ParseWith(parser, commandData)
.SetCallerPlayer(callerPlayer)
.SetTargetPlayers(targetPlayers);
commandParser.FreeSelf();
// Report or execute
if (!callInfo.IsSuccessful())
{
ReportError(callerPlayer, callInfo);
return callInfo;
}
Executed(callInfo);
if (commandData.requiresTarget)
{
for (i = 0; i < targetPlayers.length; i += 1) {
ExecutedFor(targetPlayers[i], callInfo);
}
}
return callInfo;
}
// Reports given error to the `callerPlayer`, appropriately picking
// message color
private final function ReportError(
APLayer callerPlayer,
CommandCall callInfo)
{
local Text errorMessage;
local Color previousConsoleColor;
local ConsoleWriter console;
if (callerPlayer == none) return;
if (callInfo == none) return;
if (callInfo.IsSuccessful()) return;
// Setup console color
console = callerPlayer.Console();
previousConsoleColor = console.GetColor();
if (callInfo.GetError() == CET_EmptyTargetList) {
console.SetColor(_.color.TextWarning);
}
else {
console.SetColor(_.color.TextFailure);
}
// Send message
errorMessage = callInfo.PrintErrorMessage();
console.Write(errorMessage);
errorMessage.FreeSelf();
// Restore console color
console.SetColor(previousConsoleColor).Flush();
}
// Creates (and returns) empty `CommandCall` with given error type and
// empty error cause and reports it
private final function CommandCall MakeAndReportError(
APLayer callerPlayer,
ErrorType errorType)
{
local CommandCall dummyCall;
if (errorType == CET_None) return none;
dummyCall = class'CommandCall'.static.MakeError(errorType, callerPlayer);
ReportError(callerPlayer, dummyCall);
return dummyCall;
}
// Auxiliary method for parsing list of targeted players.
// Assumes given parser is not `none` and not in a failed state.
private final function array<APlayer> ParseTargets(
Parser parser,
APlayer callerPlayer)
{
local array<APlayer> targetPlayers;
local PlayersParser targetsParser;
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser'));
targetsParser.SetSelf(callerPlayer);
targetsParser.ParseWith(parser);
targetPlayers = targetsParser.GetPlayers();
targetsParser.FreeSelf();
return targetPlayers;
}
// TODO: This is a hack to insert new line symbol,
// this needs to be redone in a better way
private final function Text.Character GetNewLine(Text.Formatting formatting)
{
local Text.Character newLine;
newLine.codePoint = 10;
newLine.formatting = formatting;
return newLine;
}
/**
* Returns colored `Text` with auto-generated help page for the caller command.
*
* @return Auto-generated help page for the caller `Command` class.
*/
public final function Text PrintHelp()
{
local Text result, commandNameAsText, commandNameRandomCase;
local MutableText builder, subBuilder;
local Text.Formatting defaultFormatting;
defaultFormatting = _.text.FormattingFromColor(_.color.TextDefault);
builder = _.text.Empty();
// Get capitalized command name
commandNameRandomCase = _.text.FromString(commandName);
commandNameAsText = commandNameRandomCase.UpperCopy();
commandNameRandomCase.FreeSelf();
// Print header: name + basic info
builder.Append(commandNameAsText, defaultFormatting);
if (commandData.requiresTarget) {
builder.Append(T(TCMD_WITH_TARGET), defaultFormatting);
}
else {
builder.Append(T(TCMD_WITHOUT_TARGET), defaultFormatting);
}
// Print commands part
subBuilder = PrintCommands(commandNameAsText);
builder.Append(subBuilder);
_.memory.Free(subBuilder);
// Print options part
subBuilder = PrintOptions();
builder.Append(subBuilder);
_.memory.Free(subBuilder);
result = builder.Copy();
builder.FreeSelf();
return result;
}
private final function MutableText PrintCommands(Text commandNameAsText)
{
local int i;
local Text.Character newLine;
local MutableText builder, subBuilder;
local array<SubCommand> subCommands;
newLine = GetNewLine(_.text.FormattingFromColor(_.color.TextDefault));
subCommands = commandData.subCommands;
builder = _.text.Empty();
for (i = 0; i < subCommands.length; i += 1)
{
builder.AppendCharacter(newLine);
subBuilder = PrintSubCommand(commandNameAsText, subCommands[i]);
builder.AppendCharacter(newLine).Append(subBuilder);
_.memory.Free(subBuilder);
}
return builder;
}
private final function MutableText PrintOptions()
{
local int i;
local Text.Character newLine;
local MutableText builder, subBuilder;
local array<Option> options;
options = commandData.options;
if (options.length <= 0) {
return none;
}
newLine = GetNewLine(_.text.FormattingFromColor(_.color.TextDefault));
builder = _.text.Empty();
builder.AppendCharacter(newLine)
.Append(T(TOPTIONS))
.AppendCharacter(newLine);
for (i = 0; i < options.length; i += 1)
{
subBuilder = PrintOption(options[i]);
builder.AppendCharacter(newLine).Append(subBuilder);
_.memory.Free(subBuilder);
}
return builder;
}
private final function MutableText PrintSubCommand(
Text usedCommandName,
SubCommand subCommand)
{
local MutableText builder, subBuilder;
local Text.Formatting defaultFormatting, emphasisFormatting;
defaultFormatting = _.text.FormattingFromColor(_.color.TextDefault);
emphasisFormatting = _.text.FormattingFromColor(_.color.TextEmphasis);
// Command + parameters
builder = _.text.Empty().Append(usedCommandName, emphasisFormatting);
if (subCommand.name != none && !subCommand.name.IsEmpty())
{
builder.Append(T(TSPACE), defaultFormatting)
.Append(subCommand.name, emphasisFormatting);
}
subBuilder = PrintParameters(subCommand.required, subCommand.optional);
builder.Append(T(TSPACE), defaultFormatting).Append(subBuilder);
_.memory.Free(subBuilder);
// Text description
builder.AppendCharacter(GetNewLine(defaultFormatting))
.Append(T(TINDENT), defaultFormatting)
.Append(subCommand.description, defaultFormatting);
return builder;
}
private final function MutableText PrintOption(Option option)
{
local Text.Character shortName;
local MutableText builder, subBuilder;
local Text.Formatting defaultFormatting, emphasisFormatting;
defaultFormatting = _.text.FormattingFromColor(_.color.TextDefault);
emphasisFormatting = _.text.FormattingFromColor(_.color.TextEmphasis);
// Option name
shortName = option.shortName;
shortName.formatting = emphasisFormatting;
builder = _.text.Empty()
.Append(T(TKEY), emphasisFormatting) // "-"
.AppendCharacter(shortName)
.Append(T(TCOMMA_SPACE), defaultFormatting) //", "
.Append(T(TDOUBLE_KEY), emphasisFormatting) //"--"
.Append(option.longName, emphasisFormatting)
.Append(T(TSPACE), defaultFormatting);
// Possible options
if (option.required.length != 0 || option.optional.length != 0)
{
subBuilder = PrintParameters(option.required, option.optional);
builder.Append(subBuilder);
_.memory.Free(subBuilder);
// If there actually were options - start a new line
builder.AppendCharacter(GetNewLine(defaultFormatting))
.Append(T(TINDENT), defaultFormatting);
}
// Text description
return builder.Append(option.description, defaultFormatting);
}
private final function MutableText PrintParameters(
array<Parameter> required,
array<Parameter> optional)
{
local int i;
local MutableText builder, subBuilder;
local Text.Formatting defaultFormatting;
defaultFormatting = _.text.FormattingFromColor(_.color.TextDefault);
builder = _.text.Empty();
// Print required
for (i = 0; i < required.length; i += 1)
{
subBuilder = PrintParameter(required[i]);
builder.Append(subBuilder);
_.memory.Free(subBuilder);
if (i < required.length - 1) {
builder.Append(T(TSPACE), defaultFormatting);
}
}
if (optional.length <= 0) {
return builder;
}
// Print optional
builder.Append(T(TSPACE), defaultFormatting)
.Append(T(TOPEN_BRACKET), defaultFormatting);
for (i = 0; i < optional.length; i += 1)
{
subBuilder = PrintParameter(optional[i]);
builder.Append(subBuilder);
_.memory.Free(subBuilder);
if (i < optional.length - 1) {
builder.Append(T(TSPACE), defaultFormatting);
}
}
builder.Append(T(TCLOSE_BRACKET), defaultFormatting);
return builder;
}
private final function MutableText PrintParameter(Parameter parameter)
{
local MutableText builder;
local Text.Formatting defaultFormatting, typeFormatting;
defaultFormatting = _.text.FormattingFromColor(_.color.TextDefault);
switch (parameter.type)
{
case CPT_Boolean:
typeFormatting = _.text.FormattingFromColor(_.color.TypeBoolean);
break;
case CPT_Integer:
typeFormatting = _.text.FormattingFromColor(_.color.TypeNumber);
break;
case CPT_Number:
typeFormatting = _.text.FormattingFromColor(_.color.TypeNumber);
break;
case CPT_Text:
typeFormatting = _.text.FormattingFromColor(_.color.TypeString);
break;
case CPT_Object:
typeFormatting = _.text.FormattingFromColor(_.color.TypeLiteral);
break;
case CPT_Array:
typeFormatting = _.text.FormattingFromColor(_.color.TypeLiteral);
break;
default:
}
builder = _.text.Empty().Append(parameter.displayName, typeFormatting);
if (parameter.allowsList) {
builder.Append(T(TPLUS), typeFormatting);
}
return builder;
}
private final function Text PrintBooleanType(PreferredBooleanFormat booleanType)
{
switch (booleanType)
{
case PBF_TrueFalse:
return T(TBOOLEAN_TRUE_FALSE);
case PBF_EnableDisable:
return T(TBOOLEAN_ENABLE_DISABLE);
case PBF_OnOff:
return T(TBOOLEAN_ON_OFF);
case PBF_YesNo:
return T(TBOOLEAN_YES_NO);
default:
}
return T(TBOOLEAN);
}
defaultproperties
{
TSPACE = 0
stringConstants(0) = " "
TPLUS = 1
stringConstants(1) = "(+)"
TOPEN_BRACKET = 2
stringConstants(2) = "["
TCLOSE_BRACKET = 3
stringConstants(3) = "]"
TKEY = 4
stringConstants(4) = "-"
TDOUBLE_KEY = 5
stringConstants(5) = "--"
TCOMMA_SPACE = 6
stringConstants(6) = ", "
TINDENT = 7
stringConstants(7) = " "
TBOOLEAN = 8
stringConstants(8) = "boolean"
TBOOLEAN_TRUE_FALSE = 9
stringConstants(9) = "true/false"
TBOOLEAN_ENABLE_DISABLE = 10
stringConstants(10) = "enable/disable"
TBOOLEAN_ON_OFF = 11
stringConstants(11) = "on/off"
TBOOLEAN_YES_NO = 12
stringConstants(12) = "yes/no"
TCMD_WITH_TARGET = 13
stringConstants(13) = ": This command requires target to be specified."
TCMD_WITHOUT_TARGET = 14
stringConstants(14) = ": This command does not require target to be specified."
TOPTIONS = 15
stringConstants(15) = "OPTIONS"
// Under normal conditions we only create one instance of each, so
// there is no need to object pools
usesObjectPool = false
}

393
sources/Commands/CommandCall.uc

@ -0,0 +1,393 @@
/**
* This object describes a call attempt for one of the `Command`s.
* `Command`s are meant to be be executed from user's console input,
* so this object should only be created while parsing their input. Any other
* use of this object is not guaranteed to be supported.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandCall extends AcediaObject
dependson(Command);
// Once this value is set to `true`, the command call is considered fully
// described and will prevent any changes to it's internal state
// (except deallocation).
var private bool locked;
// Player who initiated the call and targeted players (if applicable)
var private APlayer callerPlayer;
var private array<APlayer> targetPlayers;
// Specified sub-command and parameters/options
var private Text subCommandName;
var private AssociativeArray commandParameters, commandOptions;
// Errors that occurred during command call processing are described by
// error type and optional error textual name of the object
// (parameter, option, etc.) that caused it.
var private Command.ErrorType parsingError;
var private Text errorCause;
var public const int TBAD_PARSER, TNOSUB_COMMAND, TNO_REQ_PARAM;
var public const int TNO_REQ_PARAM_FOR_OPTION, TUNKNOW_NOPTION;
var public const int TUNKNOWN_SHORT_OPTION, TREPEATED_OPTION, TUNUSED_INPUT;
var public const int TMULTIPLE_OPTIONS_WITH_PARAMS;
var public const int TINCORRECT_TARGET, TEMPTY_TARGETS;
protected function Constructor()
{
// We simply take ownership and record into `commandParameters` whatever
// `AssociativeArray` was passed to us, but fill (and therefore create)
// `commandOptions` ourselves.
commandOptions = _.collections.EmptyAssociativeArray();
}
protected function Finalizer()
{
_.memory.Free(commandParameters);
_.memory.Free(commandOptions);
_.memory.Free(subCommandName);
_.memory.Free(errorCause);
commandParameters = none;
commandOptions = none;
subCommandName = none;
errorCause = none;
parsingError = CET_None;
targetPlayers.length = 0;
locked = false;
}
/**
* Method for producing erroneous `CommandCall` for the needs of
* error reporting.
*
* @param type Type of error resulting `CommandCall` must have.
* @param callerPlayer Player that caused erroneous command call.
* @return `CommandCall` with specified error type and `APlayer`.
*/
public final static function CommandCall MakeError(
Command.ErrorType type,
APlayer callerPlayer)
{
return CommandCall(__().memory.Allocate(class'CommandCall'))
.DeclareError(type)
.SetCallerPlayer(callerPlayer);
}
/**
* Put caller `CommandCall` into erroneous state.
* Does nothing after `CommandCall` was locked for change (see `Finish()`).
*
* @param type Type of error caller `CommandCall` must have.
* Once error (not `CET_None`) was set - calling this method with
* `CET_None` to erase error will not work.
* @param cause Textual description of offender command part to supplement
* error report (will be used when reporting error to the caller).
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall DeclareError(
Command.ErrorType type,
optional Text cause)
{
if (locked) return self;
if (parsingError != CET_None) return self;
parsingError = type;
_.memory.Free(errorCause);
errorCause = none;
if (cause != none) {
errorCause = cause.Copy();
}
return self;
}
/**
* Checks if caller `CommandCall` is in erroneous state.
*
* @return `true` if `CommandCall` has not recorded any errors so far and
* `false` otherwise.
*/
public final function bool IsSuccessful()
{
return parsingError == CET_None;
}
/**
* Returns current error type (including `CET_None` if there were no errors).
*
* @return Error type for caller `CommandCall` error.
*/
public final function Command.ErrorType GetError()
{
return parsingError;
}
/**
* In case there were any errors - returns textual description of offender
* command part. Mostly used for reporting errors to players.
*
* @return Textual description of command part that caused an error.
*/
public final function Text GetErrorCause()
{
if (errorCause != none) {
return errorCause.Copy();
}
return none;
}
/**
* After this method is called - changes to the `CommandCall` will be
* prevented.
*
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall Finish()
{
locked = true;
return self;
}
/**
* Returns player, that initiated command, that produced caller `CommandCall`.
*
* @return Player, that initiated command, that produced caller `CommandCall`
*/
public final function APlayer GetCallerPlayer()
{
return callerPlayer;
}
/**
* Sets player, that initiated command, that produced caller `CommandCall`.
* Does nothing after `CommandCall` was locked for change (see `Finish()`).
*
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall SetCallerPlayer(APlayer player)
{
callerPlayer = player;
return self;
}
/**
* Returns players that were targeted by command that produced caller
* `CommandCall`.
*
* @return Players, targeted by caller `CommandCall`.
*/
public final function array<APlayer> GetTargetPlayers()
{
return targetPlayers;
}
/**
* Sets players, targeted by command that produced caller `CommandCall`.
* Does nothing after `CommandCall` was locked for change (see `Finish()`).
*
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall SetTargetPlayers(array<APlayer> newTargets)
{
if (!locked) {
targetPlayers = newTargets;
}
return self;
}
/**
* Returns picked sub-command of command that produced caller `CommandCall`.
*
* @return Sub-command of command that produced caller `CommandCall`.
* Returns stored value that will be deallocated along with
* caller `CommandCall` - do not deallocate returned `Text` manually.
*/
public final function Text GetSubCommand()
{
return subCommandName;
}
/**
* Sets sub-command of command that produced caller `CommandCall`.
* Does nothing after `CommandCall` was locked for change (see `Finish()`).
*
* @param newSubCommandName New sub command name.
* Copy of passed object will be stored.
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall SetSubCommand(Text newSubCommandName)
{
if (!locked)
{
_.memory.Free(subCommandName);
subCommandName = newSubCommandName.Copy();
}
return self;
}
/**
* Returns parameters of command that produced caller `CommandCall`.
*
* @return Parameters of command that produced caller `CommandCall`.
* Returns stored value that will be deallocated along with
* caller `CommandCall` - do not deallocate returned `AssociativeArray`
* manually.
*/
public final function AssociativeArray GetParameters()
{
return commandParameters;
}
/**
* Sets parameters of command that produced caller `CommandCall`.
* Does nothing after `CommandCall` was locked for change (see `Finish()`).
*
* @param parameters New set of parameters. Passed value will be managed by
* caller `CommandCall` and should not be deallocated manually after
* calling `SetParameters()`.
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall SetParameters(AssociativeArray parameters)
{
if (!locked)
{
_.memory.Free(commandParameters);
commandParameters = parameters;
}
return self;
}
/**
* Returns options of command that produced caller `CommandCall`.
*
* If option without parameters was specified - it will be recorded as
* a key with value `none`.
* If option has parameters - `AssociativeArray` with them will be
* recorded as value instead.
*
* @return Options of command that produced caller `CommandCall`.
* Returns stored value that will be deallocated along with
* caller `CommandCall` - do not deallocate returned `AssociativeArray`
* manually.
*/
public final function AssociativeArray GetOptions()
{
return commandOptions;
}
/**
* Sets option parameters of command that produced caller `CommandCall`.
* Does nothing after `CommandCall` was locked for change (see `Finish()`).
*
* For recording options without parameters simply pass `none` instead of them.
*
* @param option Option to record (along with it's possible parameters).
* @param parameters Option parameters. Passed value will be managed by
* caller `CommandCall` and should not be deallocated manually after
* calling `SetParameters()`.
* Pass `none` if option has no parameters.
* @return Caller `CommandCall` to allow for method chaining.
*/
public final function CommandCall SetOptionParameters(
Command.Option option,
optional AssociativeArray parameters)
{
if (locked) return self;
if (commandOptions == none) return self;
commandOptions.SetItem(option.longName, parameters, true);
return self;
}
/**
* Prints error message as a human-readable message that can be reported to
* the caller player.
*
* In case there was no error - empty text is returned.
*
* @return Error message in a human-readable form.
*/
public final function Text PrintErrorMessage()
{
local Text result;
local MutableText builder;
builder = _.text.Empty();
switch (parsingError)
{
case CET_BadParser:
builder.Append(T(TBAD_PARSER));
break;
case CET_NoSubCommands:
builder.Append(T(TNOSUB_COMMAND));
break;
case CET_NoRequiredParam:
builder.Append(T(TNO_REQ_PARAM)).Append(errorCause);
break;
case CET_NoRequiredParamForOption:
builder.Append(T(TNO_REQ_PARAM_FOR_OPTION)).Append(errorCause);
break;
case CET_UnknownOption:
builder.Append(T(TUNKNOW_NOPTION)).Append(errorCause);
break;
case CET_UnknownShortOption:
builder.Append(T(TUNKNOWN_SHORT_OPTION));
break;
case CET_RepeatedOption:
builder.Append(T(TREPEATED_OPTION)).Append(errorCause);
break;
case CET_UnusedCommandParameters:
builder.Append(T(TUNUSED_INPUT)).Append(errorCause);
break;
case CET_MultipleOptionsWithParams:
builder.Append(T(TMULTIPLE_OPTIONS_WITH_PARAMS)).Append(errorCause);
break;
case CET_IncorrectTargetList:
builder.Append(T(TINCORRECT_TARGET)).Append(errorCause);
break;
case CET_EmptyTargetList:
builder.Append(T(TEMPTY_TARGETS)).Append(errorCause);
break;
default:
}
result = builder.Copy();
builder.FreeSelf();
return result;
}
defaultproperties
{
TBAD_PARSER = 0
stringConstants(0) = "Internal error occurred: invalid parser."
TNOSUB_COMMAND = 1
stringConstants(1) = "Ill defined command: no subcommands"
TNO_REQ_PARAM = 2
stringConstants(2) = "Missing required parameter: "
TNO_REQ_PARAM_FOR_OPTION = 3
stringConstants(3) = "Missing required parameter for option: "
TUNKNOW_NOPTION = 4
stringConstants(4) = "Invalid option specified: "
TUNKNOWN_SHORT_OPTION = 5
stringConstants(5) = "Invalid short option specified."
TREPEATED_OPTION = 6
stringConstants(6) = "Option specified several times: "
TUNUSED_INPUT = 7
stringConstants(7) = "Part of command could not be parsed: "
TMULTIPLE_OPTIONS_WITH_PARAMS = 8
stringConstants(8) = "Multiple short options in one declarations require parameters:"
TINCORRECT_TARGET = 9
stringConstants(9) = "Target players are incorrectly specified."
TEMPTY_TARGETS = 10
stringConstants(10) = "List of target players is empty."
}

883
sources/Commands/CommandDataBuilder.uc

@ -0,0 +1,883 @@
/**
* Utility class that provides developers with a simple interface to
* prepare data that describes command's parameters and options.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandDataBuilder extends AcediaObject
dependson(Command);
/**
* `CommandDataBuilder` should be able to fill information about:
* 1. subcommands and their parameters;
* 2. options and their parameters.
* As far as user is concerned, the process of filling both should be
* identical. Therefore we will store all defined data in two ways:
* 1. Selected data: data about parameters for subcommand/option that is
* currently being filled;
* 2. Prepared data: data that was already filled as "selected data" then
* stored in these records. Whenever we want to switch to filling
* another subcommand/option or return already prepared data we must
* dump "selected data" into "prepared data" first and then return
* the latter.
*
* Overall, intended flow for creating a new sub-command or option is to
* select either, fill it with data with public methods `Param...()` into
* "selected data" and then copy it into "prepared data"
* (through a `RecordSelection()` method below).
*/
// "Prepared data"
var private array<Command.SubCommand> subcommands;
var private array<Command.Option> options;
var private bool requiresTarget;
// Auxiliary arrays signifying that we've started adding optional
// parameters into appropriate `subcommands` and `options`.
// All optional parameters must follow strictly after required parameters
// and so, after user have started adding optional parameters to
// subcommand/option, we prevent them from adding required ones
// (to that particular command/option).
var private array<byte> subcommandsIsOptional;
var private array<byte> optionsIsOptional;
// "Selected data"
// `false` means we have selected sub-command, `true` - option
var private bool selectedItemIsOption;
// `name` for sub-commands, `longName` for options
var private Text selectedItemName;
// Description of selected sub-command/option
var private Text selectedDescription;
// Are we filling optional parameters (`true`)? Or required ones (`false`)?
var private bool selectionIsOptional;
// Array of parameters we are currently filling (either required or optional)
var private array<Command.Parameter> selectedParameterArray;
protected function Constructor()
{
// Fill empty subcommand (no special key word) by default
SelectSubCommand(P(""));
}
protected function Finalizer()
{
subcommands.length = 0;
subcommandsIsOptional.length = 0;
options.length = 0;
optionsIsOptional.length = 0;
selectedParameterArray.length = 0;
selectedItemName = none;
selectedDescription = none;
requiresTarget = false;
selectedItemIsOption = false;
selectionIsOptional = false;
}
// Find index of sub-command with a given name `name` in `subcommands`.
// `-1` if there's not sub-command with such name.
// Case-sensitive.
private final function int FindSubCommandIndex(Text name)
{
local int i;
if (name == none) {
return -1;
}
for (i = 0; i < subcommands.length; i += 1)
{
if (name.Compare(subcommands[i].name)) {
return i;
}
}
return -1;
}
// Find index of option with a given name `name` in `options`.
// `-1` if there's not sub-command with such name.
// Case-sensitive.
private final function int FindOptionIndex(Text longName)
{
local int i;
if (longName == none) {
return -1;
}
for (i = 0; i < options.length; i += 1)
{
if (longName.Compare(options[i].longName)) {
return i;
}
}
return -1;
}
// Creates an empty selection record for subcommand or option with
// name (long name) `name`.
// Doe not check whether subcommand/option with that name already exists.
// Copies passed `name`, assumes that it is not `none`.
private final function MakeEmptySelection(Text name, bool selectedOption)
{
selectedItemIsOption = selectedOption;
selectedItemName = name.Copy();
selectedDescription = none;
selectedParameterArray.length = 0;
selectionIsOptional = false;
}
// Select sub-command with a given name `name` from `subcommands`.
// If there is no command with specified name `name` in prepared data -
// creates new record in selection, otherwise copies previously saved data.
// Automatically saves previously selected data into prepared data.
// Copies `name` if it has to create new record.
private final function SelectSubCommand(Text name)
{
local int subcommandIndex;
if (name == none) return;
if ( !selectedItemIsOption && selectedItemName != none
&& selectedItemName.Compare(name))
{
return;
}
RecordSelection();
subcommandIndex = FindSubCommandIndex(name);
if (subcommandIndex < 0)
{
MakeEmptySelection(name, false);
return;
}
// Load appropriate prepared data, if it exists for
// sub-command with name `name`
selectedItemIsOption = false;
selectedItemName = subcommands[subcommandIndex].name;
selectedDescription = subcommands[subcommandIndex].description;
selectionIsOptional = subcommandsIsOptional[subcommandIndex] > 0;
if (selectionIsOptional) {
selectedParameterArray = subcommands[subcommandIndex].optional;
}
else {
selectedParameterArray = subcommands[subcommandIndex].required;
}
}
// Select option with a given long name `longName` from `options`.
// If there is no option with specified `longName` in prepared data -
// creates new record in selection, otherwise copies previously saved data.
// Automatically saves previously selected data into prepared data.
// Copies `name` if it has to create new record.
private final function SelectOption(Text longName)
{
local int optionIndex;
if (longName == none) return;
if ( selectedItemIsOption && selectedItemName != none
&& selectedItemName.Compare(longName))
{
return;
}
RecordSelection();
optionIndex = FindOptionIndex(longName);
if (optionIndex < 0)
{
MakeEmptySelection(longName, true);
return;
}
// Load appropriate prepared data, if it exists for
// option with long name `longName`
selectedItemIsOption = true;
selectedItemName = options[optionIndex].longName;
selectedDescription = options[optionIndex].description;
selectionIsOptional = optionsIsOptional[optionIndex] > 0;
if (selectionIsOptional) {
selectedParameterArray = options[optionIndex].optional;
}
else {
selectedParameterArray = options[optionIndex].required;
}
}
// Saves currently selected data into prepared data.
private final function RecordSelection()
{
if (selectedItemName == none) {
return;
}
if (selectedItemIsOption) {
RecordSelectedOption();
}
else {
RecordSelectedSubCommand();
}
}
// Saves selected sub-command into prepared records.
// Assumes that command and not an option is selected.
private final function RecordSelectedSubCommand()
{
local int selectedSubCommandIndex;
local Command.SubCommand newSubcommand;
if (selectedItemName == none) return;
selectedSubCommandIndex = FindSubCommandIndex(selectedItemName);
if (selectedSubCommandIndex < 0)
{
selectedSubCommandIndex = subcommands.length;
subcommands[selectedSubCommandIndex] = newSubcommand;
}
subcommands[selectedSubCommandIndex].name = selectedItemName;
subcommands[selectedSubCommandIndex].description = selectedDescription;
if (selectionIsOptional)
{
subcommands[selectedSubCommandIndex].optional = selectedParameterArray;
subcommandsIsOptional[selectedSubCommandIndex] = 1;
}
else
{
subcommands[selectedSubCommandIndex].required = selectedParameterArray;
subcommandsIsOptional[selectedSubCommandIndex] = 0;
}
}
// Saves currently selected option into prepared records.
// Assumes that option and not an command is selected.
private final function RecordSelectedOption()
{
local int selectedOptionIndex;
local Command.Option newOption;
if (selectedItemName == none) return;
selectedOptionIndex = FindOptionIndex(selectedItemName);
if (selectedOptionIndex < 0)
{
selectedOptionIndex = options.length;
options[selectedOptionIndex] = newOption;
}
options[selectedOptionIndex].longName = selectedItemName;
options[selectedOptionIndex].description = selectedDescription;
if (selectionIsOptional)
{
options[selectedOptionIndex].optional = selectedParameterArray;
optionsIsOptional[selectedOptionIndex] = 1;
}
else
{
options[selectedOptionIndex].required = selectedParameterArray;
optionsIsOptional[selectedOptionIndex] = 0;
}
}
/**
* Method to use to start defining a new sub-command.
*
* Does two things:
* 1. Creates new sub-command with a given name (if it's missing);
* 2. Selects sub-command with name `name` to add parameters to.
*
* @param name Name of the sub-command user wants to define,
* case-sensitive. Variable will be copied.
* If `none` is passed, this method will do nothing.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder SubCommand(Text name)
{
SelectSubCommand(name);
return self;
}
// Validates names (printing errors in case of failure) for the option.
// Long name must be at least 2 characters long.
// Short name must be either:
// 1. exactly one character long;
// 2. `none`, which leads to deriving `shortName` from `longName`
// as a first character.
// Anything else will result in logging a failure and rejection of
// the option altogether.
// Returns `none` if validation failed and chosen short name otherwise
// (if `shortName` was used for it - it's value will be copied).
private final function Text.Character GetValidShortName(
Text longName,
Text shortName)
{
// Validate `longName`
if (longName == none) {
return _.text.GetInvalidCharacter();
}
if (longName.GetLength() < 2)
{
_.logger.Failure("Command" @ self.class @ "is trying to register"
@ "an option with a name that is way too short (<2 characters)."
@ "Option will be discarded:" @ longName.ToPlainString());
return _.text.GetInvalidCharacter();
}
// Validate `shortName`,
// deriving if from `longName` if necessary & possible
if (shortName == none)
{
return longName.GetCharacter(0);
}
if (shortName.IsEmpty() || shortName.GetLength() > 1)
{
_.logger.Failure("Command" @ self.class @ "is trying to register"
@ "an option with a short name that doesn't consist of just"
@ "one character. Option will be discarded:"
@ longName.ToPlainString());
return _.text.GetInvalidCharacter();
}
return shortName.GetCharacter(0);
}
// Checks that if any option record has a long/short name from a given pair of
// names (`longName`, `shortName`), then it also has another one.
//
// i.e. we cannot have several options with identical names:
// (--silent, -s) and (--sick, -s).
private final function bool VerifyNoOptionNamingConflict(
Text longName,
Text.Character shortName)
{
local int i;
// To make sure we will search through the up-to-date `options`,
// record selection into prepared records.
RecordSelection();
for (i = 0; i < options.length; i += 1)
{
// Is same long name, but different long names?
if ( !_.text.AreEqual(shortName, options[i].shortName)
&& longName.Compare(options[i].longName))
{
_ .logger.Warning("Command" @ self.class @ "is trying to register"
@ "several options with the same long name"
@ "\"" $ longName.ToPlainString()
$ "\", but different short names. This should not happen,"
@ "do not expect correct behavior.");
return true;
}
// Is same short name, but different short ones?
if ( _.text.AreEqual(shortName, options[i].shortName)
&& !longName.Compare(options[i].longName))
{
_.logger.Warning("Command" @ self.class @ "is trying to register"
@ "several options with the same short name"
@ "\"" $ _.text.CharacterToString(shortName)
$ "\", but different long names. This should not have happened,"
@ "do not expect correct behavior.");
return true;
}
}
return false;
}
/**
* Method to use to start defining a new option.
*
* Does three things:
* 1. Checks if some of the recorded options are in conflict with given
* `longName` and `shortName` (already using one and only one of them).
* 2. Creates new option with a long and short names
* (if such option is missing);
* 3. Selects option with a long name `longName` to add parameters to.
*
* @param longName Long name of the option, case-sensitive
* (for using an option in form "--...").
* Must be at least two characters long. If passed value is either `none`
* or too short, method will log an error and omits this option.
* @param shortName Short name of the option, case-sensitive
* (for using an option in form "-...").
* Must be exactly one character. If `none` value is passed
* (or the argument altogether omitted) - uses first character of
* the `longName`.
* If `shortName` is not `none` and is not exactly 1 character long -
* logs an error and omits this option.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder Option(
Text longName,
optional Text shortName)
{
local int optionIndex;
local Text.Character shortNameAsCharacter;
// Unlike for `SubCommand()`, we need to ensure that option naming is
// correct and does not conflict with existing options
// (user might attempt to add two options with same long names and
// different short ones).
shortNameAsCharacter = GetValidShortName(longName, shortName);
if ( !_.text.IsValidCharacter(shortNameAsCharacter)
|| VerifyNoOptionNamingConflict(longName, shortNameAsCharacter))
{
// ^ `GetValidShortName()` and `VerifyNoOptionNamingConflict()`
// are responsible for logging warnings/errors
return self;
}
SelectOption(longName);
// Set short name for new options
optionIndex = FindOptionIndex(longName);
if (optionIndex < 0)
{
// We can only be here if option was created for the first time
RecordSelection();
// So now it cannot fail
optionIndex = FindOptionIndex(longName);
options[optionIndex].shortName = shortNameAsCharacter;
}
return self;
}
/**
* Adds description to the selected sub-command / option.
*
* Previous description is discarded (default description is empty).
*
* Does nothing if nothing is selected.
*
* @param description New description of selected sub-command / option.
* Variable will be copied.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder Describe(Text description)
{
if (selectedDescription == description) {
return self;
}
_.memory.Free(selectedDescription);
if (description != none) {
selectedDescription = description.Copy();
}
return self;
}
/**
* Makes caller builder to mark `Command.Data` under construction to require
* a player target.
*
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder RequireTarget()
{
requiresTarget = true;
return self;
}
/**
* Any parameters added to currently selected sub-command / option after
* calling this method will be marked as optional.
*
* Further calls when the same sub-command / option is selected do nothing.
*
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder OptionalParams()
{
if (selectionIsOptional) {
return self;
}
// Record all required parameters first, otherwise there would be no way
// to distinguish between them and optional parameters
RecordSelection();
selectionIsOptional = true;
selectedParameterArray.length = 0;
return self;
}
/**
* Returns data that has been constructed so far by
* the caller `CommandDataBuilder`.
*
* Does not reset progress.
*
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function Command.Data GetData()
{
local Command.Data newData;
RecordSelection();
newData.subcommands = subcommands;
newData.options = options;
newData.requiresTarget = requiresTarget;
return newData;
}
// Adds new parameter to selected sub-command / option
private final function PushParameter(Command.Parameter newParameter)
{
selectedParameterArray[selectedParameterArray.length] = newParameter;
}
// Fills `Command.ParameterType` struct with given values
// (except boolean format). Assumes `displayName != none`.
private final function Command.Parameter NewParameter(
Text displayName,
Command.ParameterType parameterType,
bool isListParameter,
optional Text variableName)
{
local Command.Parameter newParameter;
newParameter.displayName = displayName.Copy();
newParameter.type = parameterType;
newParameter.allowsList = isListParameter;
if (variableName != none) {
newParameter.variableName = variableName.Copy();
}
else {
newParameter.variableName = displayName;
}
return newParameter;
}
/**
* Adds new boolean parameter (required or optional depends on whether
* `RequireTarget()` call happened) to the currently selected
* sub-command / option.
*
* Only fails if provided `name` is `none`.
*
* @param name Name of the parameter, will be copied
* (as it would appear in the generated help info).
*
* @param format Preferred format of boolean values.
* Command parser will still accept boolean values in any form,
* this setting only affects how parameter will be displayed in
* generated help.
* @param variableName Name of the variable that will store this
* parameter's value in `AssociativeArray` after user's command input
* is parsed. Provided value will be copied.
* If left `none`, - will coincide with `name` parameter.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder ParamBoolean(
Text name,
optional Command.PreferredBooleanFormat format,
optional Text variableName)
{
local Command.Parameter newParam;
if (name == none) {
return self;
}
newParam = NewParameter(name, CPT_Boolean, false, variableName);
newParam.booleanFormat = format;
PushParameter(newParam);
return self;
}
/**
* Adds new boolean list parameter (required or optional depends on whether
* `RequireTarget()` call happened) to the currently selected
* sub-command / option.
*
* Only fails if provided `name` is `none`.
*
* @param name Name of the parameter, will be copied
* (as it would appear in the generated help info).
* @param format Preferred format of boolean values.
* Command parser will still accept boolean values in any form,
* this setting only affects how parameter will be displayed in
* generated help.
* @param variableName Name of the variable that will store this
* parameter's value in `AssociativeArray` after user's command input
* is parsed. Provided value will be copied.
* If left `none`, - will coincide with `name` parameter.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder ParamBooleanList(
Text name,
optional Command.PreferredBooleanFormat format,
optional Text variableName)
{
local Command.Parameter newParam;
if (name == none) {
return self;
}
newParam = NewParameter(name, CPT_Boolean, true, variableName);
newParam.booleanFormat = format;
PushParameter(newParam);
return self;
}
/**
* Adds new integer parameter (required or optional depends on whether
* `RequireTarget()` call happened) to the currently selected
* sub-command / option.
*
* Only fails if provided `name` is `none`.
*
* @param name Name of the parameter, will be copied
* (as it would appear in the generated help info).
* @param variableName Name of the variable that will store this
* parameter's value in `AssociativeArray` after user's command input
* is parsed. Provided value will be copied.
* If left `none`, - will coincide with `name` parameter.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder ParamInteger(
Text name,
optional Text variableName)
{
if (name == none) {
return self;
}
PushParameter(NewParameter(name, CPT_Integer, false, variableName));
return self;
}
/**
* Adds new integer list parameter (required or optional depends on whether
* `RequireTarget()` call happened) to the currently selected
* sub-command / option.
*
* Only fails if provided `name` is `none`.
*
* @param name Name of the parameter, will be copied
* (as it would appear in the generated help info).
* @param variableName Name of the variable that will store this
* parameter's value in `AssociativeArray` after user's command input
* is parsed. Provided value will be copied.
* If left `none`, - will coincide with `name` parameter.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder ParamIntegerList(
Text name,
optional Text variableName)
{
if (name == none) {
return self;
}
PushParameter(NewParameter(name, CPT_Integer, true, variableName));
return self;
}
/**
* Adds new numeric parameter (required or optional depends on whether
* `RequireTarget()` call happened) to the currently selected
* sub-command / option.
*
* Only fails if provided `name` is `none`.
*
* @param name Name of the parameter, will be copied
* (as it would appear in the generated help info).
* @param variableName Name of the variable that will store this
* parameter's value in `AssociativeArray` after user's command input
* is parsed. Provided value will be copied.
* If left `none`, - will coincide with `name` parameter.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder ParamNumber(
Text name,
optional Text variableName)
{
if (name == none) {
return self;
}
PushParameter(NewParameter(name, CPT_Number, false, variableName));
return self;
}
/**
* Adds new numeric list parameter (required or optional depends on whether
* `RequireTarget()` call happened) to the currently selected
* sub-command / option.
*
* Only fails if provided `name` is `none`.
*
* @param name Name of the parameter, will be copied
* (as it would appear in the generated help info).
* @param variableName Name of the variable that will store this
* parameter's value in `AssociativeArray` after user's command input
* is parsed. Provided value will be copied.
* If left `none`, - will coincide with `name` parameter.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder ParamNumberList(
Text name,
optional Text variableName)
{
if (name == none) {
return self;
}
PushParameter(NewParameter(name, CPT_Number, true, variableName));
return self;
}
/**
* Adds new text parameter (required or optional depends on whether
* `RequireTarget()` call happened) to the currently selected
* sub-command / option.
*
* Only fails if provided `name` is `none`.
*
* @param name Name of the parameter, will be copied
* (as it would appear in the generated help info).
* @param variableName Name of the variable that will store this
* parameter's value in `AssociativeArray` after user's command input
* is parsed. Provided value will be copied.
* If left `none`, - will coincide with `name` parameter.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder ParamText(
Text name,
optional Text variableName)
{
if (name == none) {
return self;
}
PushParameter(NewParameter(name, CPT_Text, false, variableName));
return self;
}
/**
* Adds new text list parameter (required or optional depends on whether
* `RequireTarget()` call happened) to the currently selected
* sub-command / option.
*
* Only fails if provided `name` is `none`.
*
* @param name Name of the parameter, will be copied
* (as it would appear in the generated help info).
* @param variableName Name of the variable that will store this
* parameter's value in `AssociativeArray` after user's command input
* is parsed. Provided value will be copied.
* If left `none`, - will coincide with `name` parameter.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder ParamTextList(
Text name,
optional Text variableName)
{
if (name == none) {
return self;
}
PushParameter(NewParameter(name, CPT_Text, true, variableName));
return self;
}
/**
* Adds new object parameter (required or optional depends on whether
* `RequireTarget()` call happened) to the currently selected
* sub-command / option.
*
* Only fails if provided `name` is `none`.
*
* @param name Name of the parameter, will be copied
* (as it would appear in the generated help info).
* @param variableName Name of the variable that will store this
* parameter's value in `AssociativeArray` after user's command input
* is parsed. Provided value will be copied.
* If left `none`, - will coincide with `name` parameter.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder ParamObject(
Text name,
optional Text variableName)
{
if (name == none) {
return self;
}
PushParameter(NewParameter(name, CPT_Object, false, variableName));
return self;
}
/**
* Adds new parameter for list of objects (required or optional depends on
* whether `RequireTarget()` call happened) to the currently selected
* sub-command / option.
*
* Only fails if provided `name` is `none`.
*
* @param name Name of the parameter, will be copied
* (as it would appear in the generated help info).
* @param variableName Name of the variable that will store this
* parameter's value in `AssociativeArray` after user's command input
* is parsed. Provided value will be copied.
* If left `none`, - will coincide with `name` parameter.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder ParamObjectList(
Text name,
optional Text variableName)
{
if (name == none) {
return self;
}
PushParameter(NewParameter(name, CPT_Object, true, variableName));
return self;
}
/**
* Adds new array parameter (required or optional depends on whether
* `RequireTarget()` call happened) to the currently selected
* sub-command / option.
*
* Only fails if provided `name` is `none`.
*
* @param name Name of the parameter, will be copied
* (as it would appear in the generated help info).
* @param variableName Name of the variable that will store this
* parameter's value in `AssociativeArray` after user's command input
* is parsed. Provided value will be copied.
* If left `none`, - will coincide with `name` parameter.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder ParamArray(
Text name,
optional Text variableName)
{
if (name == none) {
return self;
}
PushParameter(NewParameter(name, CPT_Array, false, variableName));
return self;
}
/**
* Adds new parameter for list of arrays (required or optional depends on
* whether `RequireTarget()` call happened) to the currently selected
* sub-command / option.
*
* Only fails if provided `name` is `none`.
*
* @param name Name of the parameter, will be copied
* (as it would appear in the generated help info).
* @param variableName Name of the variable that will store this
* parameter's value in `AssociativeArray` after user's command input
* is parsed. Provided value will be copied.
* If left `none`, - will coincide with `name` parameter.
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder ParamArrayList(
Text name,
optional Text variableName)
{
if (name == none) {
return self;
}
PushParameter(NewParameter(name, CPT_Array, true, variableName));
return self;
}
defaultproperties
{
}

800
sources/Commands/CommandParser.uc

@ -0,0 +1,800 @@
/**
* Auxiliary class for parsing user's input into a `CommandCall` based on
* a given `Command.Data`. While it's meant to be allocated for
* a `self.ParseWith()` call and deallocated right after, it can be reused
* without deallocation.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandParser extends AcediaObject
dependson(CommandCall)
dependson(Command);
/**
* `CommandParser` stores both it's state and command data, relevant to
* parsing, as it's member variables during the whole parsing process,
* instead of passing that data around in every single method.
*
* We will give a brief overview of how around 20 parsing methods below
* are interconnected.
* The only public method `ParseWith()` is used to start parsing and it
* uses `PickSubCommand()` to first try and figure out what sub command is
* intended by user's input.
* Main bulk of the work is done by `ParseParameterArrays()` method,
* for simplicity broken into two `ParseRequiredParameterArray()` and
* `ParseOptionalParameterArray()` methods that can parse parameters for both
* command itself and it's options.
* They go through arrays of required and optional parameters,
* calling `ParseParameter()` for each parameters, which in turn can make
* several calls of `ParseSingleValue()` to parse parameters' values:
* it is called once for single-valued parameters, but possibly several times
* for list parameters that can contain several values.
* So main parsing method looks something like:
* ParseParameterArrays() {
* loop ParseParameter() {
* loop ParseSingleValue()
* }
* }
* `ParseSingleValue()` is essentially that redirects it's method call to
* another, more specific, parsing method based on the parameter type.
*
* Finally, to allow users to specify options at any point in command,
* we call `TryParsingOptions()` at the beginning of every
* `ParseSingleValue()`, since option definition can appear at any place
* between parameters. We also call `TryParsingOptions()` *after* we've parsed
* all command's parameters, since that case won't be detected by parsing
* them *before* every parameter.
* `TryParsingOptions()` itself simply tries to detect "-" and "--"
* prefixes (filtering out negative numeric values) and then redirect the call
* to either of more specialized methods: `ParseLongOption()` or
* `ParseShortOption()`, that can in turn make another `ParseParameterArrays()`
* call, if specified option has parameters.
* NOTE: `ParseParameterArrays()` can only nest in itself once, since
* option declaration always interrupts previous option's parameter list.
* Rest of the methods perform simple auxiliary functions.
*/
// Parser filled with user input.
var private Parser commandParser;
// Data for sub-command specified by both command we are parsing
// and user's input; determined early during parsing.
var private Command.SubCommand pickedSubCommand;
// Options available for the command we are parsing.
var private array<Command.Option> availableOptions;
// Result variable we are filling during the parsing process,
// should be `none` outside of `self.ParseWith()` method call.
var private CommandCall nextResult;
// Describes which parameters we are currently parsing, classifying them
// as either "necessary" or "extra".
// E.g. if last require parameter is a list of integers,
// then after parsing first integer we are:
// * Still parsing required *parameter* "integer list";
// * But no more integers are *necessary* for successful parsing.
//
// Therefore we consider parameter "necessary" if the lack of it will
// result in failed parsing and "extra" otherwise.
enum ParsingTarget
{
// We are in the process of parsing required parameters, that must all
// be present.
// This case does not include parsing last required parameter: it needs
// to be treated differently to track when we change from "necessary" to
// "extra" parameters.
CPT_NecessaryParameter,
// We are parsing last necessary parameter.
CPT_LastNecessaryParameter,
// We are not parsing extra parameters that can be safely omitted.
CPT_ExtraParameter,
};
// Current `ParsingTarget`, see it's enum description for more details
var private ParsingTarget currentTarget;
// `true` means we are parsing parameters for a command's option and
// `false` means we are parsing command's own parameters
var private bool currentTargetIsOption;
// If we are parsing parameters for an option (`currentTargetIsOption == true`)
// this variable will store that option's data.
var private Command.Option targetOption;
// Last successful state of `commandParser`.
var Parser.ParserState confirmedState;
// Options we have so far encountered during parsing, necessary since we want
// to forbid specifying th same option more than once.
var private array<Command.Option> usedOptions;
// Literals that can be used as boolean values
var private array<string> booleanTrueEquivalents;
var private array<string> booleanFalseEquivalents;
protected function Finalizer()
{
Reset();
}
// Zero important variables
private final function Reset()
{
commandParser = none;
nextResult = none;
currentTarget = CPT_NecessaryParameter;
currentTargetIsOption = false;
usedOptions.length = 0;
}
// Auxiliary method for recording errors
private final function DeclareError(
Command.ErrorType type,
optional Text cause)
{
if (nextResult != none) {
nextResult.DeclareError(type, cause);
}
if (commandParser != none) {
commandParser.Fail();
}
}
// Assumes `commandParser != none`, is in successful state.
// Picks a sub command based on it's contents (parser's pointer must be
// before where subcommand's name is specified).
private final function PickSubCommand(Command.Data commandData)
{
local int i;
local MutableText candidateSubCommandName;
local Command.SubCommand emptySubCommand;
local array<Command.SubCommand> allSubCommands;
allSubCommands = commandData.subCommands;
if (allSubcommands.length == 0)
{
_.logger.Failure("`GetSubCommand()` method was called on a command"
@ class @ "with zero defined sub-commands.");
pickedSubCommand = emptySubCommand;
return;
}
// Get candidate name
confirmedState = commandParser.GetCurrentState();
commandParser.Skip().MUntil(candidateSubCommandName,, true);
// Try matching it to sub commands
pickedSubCommand = allSubcommands[0];
if (candidateSubCommandName.IsEmpty())
{
candidateSubCommandName.FreeSelf();
return;
}
for (i = 0; i < allSubcommands.length; i += 1)
{
if (candidateSubCommandName.Compare(allSubcommands[i].name))
{
candidateSubCommandName.FreeSelf();
pickedSubCommand = allSubcommands[i];
return;
}
}
// We will only reach here if we did not match any sub commands,
// meaning that whatever consumed by `candidateSubCommandName` probably
// has a different meaning.
commandParser.RestoreState(confirmedState);
}
/**
* Parses user's input given in `parser` using command's information given by
* `commandData`.
*
* @param parser `Parser`, initialized with user's input that will need
* to be parsed as a command's call.
* @param commandData Describes what parameters and options should be
* expected in user's input. `Text` values from `commandData` can be used
* inside resulting object `CommandCall`, so deallocating them can
* invalidate returned value.
* @return Results of parsing as described by `CommandCall`.
* Returned object is guaranteed to be not `none`.
*/
public final function CommandCall ParseWith(
Parser parser,
Command.Data commandData)
{
local AssociativeArray commandParameters;
// Temporary object to return `nextResult` while setting variable to `none`
local CommandCall toReturn;
Reset();
nextResult = CommandCall(_.memory.Allocate(class'CommandCall'));
if (commandData.subCommands.length == 0)
{
DeclareError(CET_NoSubCommands, none);
return nextResult;
}
if (parser == none || !parser.Ok())
{
DeclareError(CET_BadParser, none);
return nextResult;
}
commandParser = parser;
availableOptions = commandData.options;
// (subcommand) (parameters, possibly with options) and nothing else!
PickSubCommand(commandData);
nextResult.SetSubCommand(pickedSubCommand.name);
commandParameters = ParseParameterArrays( pickedSubCommand.required,
pickedSubCommand.optional);
AssertNoTrailingInput(); // make sure there is nothing else
if (commandParser.Ok()) {
nextResult.SetParameters(commandParameters);
}
else {
_.memory.Free(commandParameters);
}
// Clean up
commandParser = none;
usedOptions.length = 0;
currentTargetIsOption = false;
toReturn = nextResult;
nextResult = none;
return toReturn;
}
// Assumes `commandParser` is not `none`
// Declares an error if `commandParser` still has any input left
private final function AssertNoTrailingInput()
{
local Text remainder;
if (!commandParser.Ok()) return;
if (commandParser.Skip().GetRemainingLength() <= 0) return;
remainder = commandParser.GetRemainder();
DeclareError(CET_UnusedCommandParameters, remainder);
remainder.FreeSelf();
}
// Assumes `commandParser` is not `none`.
// Parses given required and optional parameters along with any
// possible option declarations.
// Returns `AssociativeArray` filled with (variable, parsed value) pairs.
// Failure is equal to `commandParser` entering into a failed state.
private final function AssociativeArray ParseParameterArrays(
array<Command.Parameter> requiredParameters,
array<Command.Parameter> optionalParameters)
{
local AssociativeArray parsedParameters;
if (!commandParser.Ok()) {
return none;
}
parsedParameters = _.collections.EmptyAssociativeArray();
// Parse parameters
ParseRequiredParameterArray(parsedParameters, requiredParameters);
ParseOptionalParameterArray(parsedParameters, optionalParameters);
// Parse trailing options
while (TryParsingOptions());
return parsedParameters;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses given required parameters along with any possible option
// declarations into given `parsedParameters` associative array.
private final function ParseRequiredParameterArray(
AssociativeArray parsedParameters,
array<Command.Parameter> requiredParameters)
{
local int i;
if (!commandParser.Ok()) {
return;
}
currentTarget = CPT_NecessaryParameter;
while (i < requiredParameters.length)
{
if (i == requiredParameters.length - 1) {
currentTarget = CPT_LastNecessaryParameter;
}
// Parse parameters one-by-one, reporting appropriate errors
if (!ParseParameter(parsedParameters, requiredParameters[i]))
{
// Any failure to parse required parameter leads to error
if (currentTargetIsOption)
{
DeclareError( CET_NoRequiredParamForOption,
targetOption.longName);
}
else
{
DeclareError( CET_NoRequiredParam,
requiredParameters[i].displayName);
}
return;
}
i += 1;
}
currentTarget = CPT_ExtraParameter;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses given optional parameters along with any possible option
// declarations into given `parsedParameters` associative array.
private final function ParseOptionalParameterArray(
AssociativeArray parsedParameters,
array<Command.Parameter> optionalParameters)
{
local int i;
if (!commandParser.Ok()) {
return;
}
while (i < optionalParameters.length)
{
confirmedState = commandParser.GetCurrentState();
// Parse parameters one-by-one, reporting appropriate errors
if (!ParseParameter(parsedParameters, optionalParameters[i]))
{
// Propagate errors
if (!nextResult.IsSuccessful()) {
return;
}
// Failure to parse optional parameter is fine if
// it is caused by that parameters simply missing
commandParser.RestoreState(confirmedState);
break;
}
i += 1;
}
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses one given parameter along with any possible option
// declarations into given `parsedParameters` associative array.
// Returns `true` if we've successfully parsed given parameter without
// any errors.
private final function bool ParseParameter(
AssociativeArray parsedParameters,
Command.Parameter expectedParameter)
{
local bool parsedEnough;
confirmedState = commandParser.GetCurrentState();
while (ParseSingleValue(parsedParameters, expectedParameter))
{
if (currentTarget == CPT_LastNecessaryParameter) {
currentTarget = CPT_ExtraParameter;
}
parsedEnough = true;
// We are done if there is either no more input or we only needed
// to parse a single value
if (!expectedParameter.allowsList) {
return true;
}
if (commandParser.Skip().HasFinished()) {
return true;
}
confirmedState = commandParser.GetCurrentState();
}
// We only succeeded in parsing if we've parsed enough for
// a given parameter and did not encounter any errors
if (parsedEnough && nextResult.IsSuccessful()) {
commandParser.RestoreState(confirmedState);
return true;
}
// Clean up any values `ParseSingleValue` might have recorded
parsedParameters.RemoveItem(expectedParameter.variableName);
return false;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single value for a given parameter (e.g. one integer for
// integer or integer list parameter types) along with any possible option
// declarations into given `parsedParameters` associative array.
// Returns `true` if we've successfully parsed a single value without
// any errors.
private final function bool ParseSingleValue(
AssociativeArray parsedParameters,
Command.Parameter expectedParameter)
{
// Before parsing a value we need to check if user has specified any
// options instead.
// However this might lead to errors if we are already parsing
// necessary parameters of another option:
// we must handle such situation and report an error.
if ( currentTargetIsOption && currentTarget != CPT_ExtraParameter
&& TryParsingOptions())
{
DeclareError(CET_NoRequiredParamForOption, targetOption.longName);
return false;
}
while (TryParsingOptions());
// Propagate errors after parsing options
if (!nextResult.IsSuccessful()) {
return false;
}
// Try parsing one of the variable types
if (expectedParameter.type == CPT_Boolean) {
return ParseBooleanValue(parsedParameters, expectedParameter);
}
else if (expectedParameter.type == CPT_Integer) {
return ParseIntegerValue(parsedParameters, expectedParameter);
}
else if (expectedParameter.type == CPT_Number) {
return ParseNumberValue(parsedParameters, expectedParameter);
}
else if (expectedParameter.type == CPT_Text) {
return ParseTextValue(parsedParameters, expectedParameter);
}
else if (expectedParameter.type == CPT_Object) {
return ParseObjectValue(parsedParameters, expectedParameter);
}
else if (expectedParameter.type == CPT_Array) {
return ParseArrayValue(parsedParameters, expectedParameter);
}
return false;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single boolean value into given `parsedParameters`
// associative array.
private final function bool ParseBooleanValue(
AssociativeArray parsedParameters,
Command.Parameter expectedParameter)
{
local int i;
local bool isValidBooleanLiteral;
local bool booleanValue;
local MutableText parsedLiteral;
commandParser.Skip().MUntil(parsedLiteral,, true);
if (!commandParser.Ok())
{
_.memory.Free(parsedLiteral);
return false;
}
// Try to match parsed literal to any recognizable boolean literals
for (i = 0; i < booleanTrueEquivalents.length; i += 1)
{
if (parsedLiteral.CompareToPlainString( booleanTrueEquivalents[i],
SCASE_INSENSITIVE))
{
isValidBooleanLiteral = true;
booleanValue = true;
break;
}
}
for (i = 0; i < booleanFalseEquivalents.length; i += 1)
{
if (isValidBooleanLiteral) break;
if (parsedLiteral.CompareToPlainString( booleanFalseEquivalents[i],
SCASE_INSENSITIVE))
{
isValidBooleanLiteral = true;
booleanValue = false;
}
}
parsedLiteral.FreeSelf();
if (!isValidBooleanLiteral) {
return false;
}
RecordParameter(parsedParameters, expectedParameter,
_.box.bool(booleanValue));
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single integer value into given `parsedParameters`
// associative array.
private final function bool ParseIntegerValue(
AssociativeArray parsedParameters,
Command.Parameter expectedParameter)
{
local int integerValue;
commandParser.Skip().MInteger(integerValue);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter,
_.box.int(integerValue));
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single number (float) value into given `parsedParameters`
// associative array.
private final function bool ParseNumberValue(
AssociativeArray parsedParameters,
Command.Parameter expectedParameter)
{
local float numberValue;
commandParser.Skip().MNumber(numberValue);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter,
_.box.float(numberValue));
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single `Text` value into given `parsedParameters`
// associative array.
private final function bool ParseTextValue(
AssociativeArray parsedParameters,
Command.Parameter expectedParameter)
{
local string textValue;
commandParser.Skip().MStringS(textValue);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter,
_.text.FromFormattedString(textValue));
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single JSON object into given `parsedParameters`
// associative array.
private final function bool ParseObjectValue(
AssociativeArray parsedParameters,
Command.Parameter expectedParameter)
{
local AssociativeArray objectValue;
objectValue = _.json.ParseObjectWith(commandParser);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, objectValue);
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single JSON array into given `parsedParameters`
// associative array.
private final function bool ParseArrayValue(
AssociativeArray parsedParameters,
Command.Parameter expectedParameter)
{
local DynamicArray arrayValue;
arrayValue = _.json.ParseArrayWith(commandParser);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, arrayValue);
return true;
}
// Assumes `parsedParameters` is not `none`.
// Records `value` for a given `parameter` into a given `parametersArray`.
// If parameter is not a list type - simply records `value` as value under
// `parameter.variableName` key.
// If parameter is a list type - pushed value at the end of an array,
// recorded at `parameter.variableName` key (creating it if missing).
// All recorded values are managed by `parametersArray`.
private final function RecordParameter(
AssociativeArray parametersArray,
Command.Parameter parameter,
AcediaObject value)
{
local DynamicArray parameterVariable;
if (!parameter.allowsList)
{
parametersArray.SetItem(parameter.variableName, value, true);
return;
}
parameterVariable =
DynamicArray(parametersArray.GetItem(parameter.variableName));
if (parameterVariable == none) {
parameterVariable = _.collections.EmptyDynamicArray();
}
parameterVariable.AddItem(value, true);
parametersArray.SetItem(parameter.variableName, parameterVariable, true);
}
// Assumes `commandParser` is not `none`.
// Tries to parse an option declaration (along with all of it's parameters)
// with `commandParser`.
// Returns `true` on success and `false` otherwise.
// In case of failure to detect option declaration also reverts state of
// `commandParser` to that before `TryParsingOptions()` call.
// However, if option declaration was present, but invalid (or had
// invalid parameters) parser will be left in a failed state.
private final function bool TryParsingOptions()
{
local int temporaryInt;
if (!commandParser.Ok()) return false;
confirmedState = commandParser.GetCurrentState();
// Long options
commandParser.Skip().Match(P("--"));
if (commandParser.Ok()) {
return ParseLongOption();
}
// Filter out negative numbers that start similarly to short options:
// -3, -5.7, -.9
commandParser.RestoreState(confirmedState)
.Skip().Match(P("-")).MUnsignedInteger(temporaryInt, 10, 1);
if (commandParser.Ok())
{
commandParser.RestoreState(confirmedState);
return false;
}
commandParser.RestoreState(confirmedState).Skip().Match(P("-."));
if (commandParser.Ok())
{
commandParser.RestoreState(confirmedState);
return false;
}
// Short options
commandParser.RestoreState(confirmedState).Skip().Match(P("-"));
if (commandParser.Ok()) {
return ParseShortOption();
}
commandParser.RestoreState(confirmedState);
return false;
}
// Assumes `commandParser` is not `none`.
// Tries to parse a long option name along with all of it's
// possible parameters with `commandParser`.
// Returns `true` on success and `false` otherwise. At the point this
// method is called, option declaration is already assumed to be detected
// and any failure implies parsing error (ending in failed `CommandCall`).
private final function bool ParseLongOption()
{
local int i, optionIndex;
local MutableText optionName;
commandParser.MUntil(optionName,, true);
if (!commandParser.Ok()) {
return false;
}
while (optionIndex < availableOptions.length)
{
if (optionName.Compare(availableOptions[optionIndex].longName)) break;
optionIndex += 1;
}
if (optionIndex >= availableOptions.length)
{
DeclareError(CET_UnknownOption, optionName);
optionName.FreeSelf();
return false;
}
for (i = 0; i < usedOptions.length; i += 1)
{
if (optionName.Compare(usedOptions[i].longName))
{
DeclareError(CET_RepeatedOption, optionName);
optionName.FreeSelf();
return false;
}
}
//usedOptions[usedOptions.length] = availableOptions[optionIndex];
optionName.FreeSelf();
return ParseOptionParameters(availableOptions[optionIndex]);
}
// Assumes `commandParser` and `nextResult` are not `none`.
// Tries to parse a short option name along with all of it's
// possible parameters with `commandParser`.
// Returns `true` on success and `false` otherwise. At the point this
// method is called, option declaration is already assumed to be detected
// and any failure implies parsing error (ending in failed `CommandCall`).
private final function bool ParseShortOption()
{
local int i;
local bool pickedOptionWithParameters;
local MutableText optionsList;
commandParser.MUntil(optionsList,, true);
if (!commandParser.Ok())
{
optionsList.FreeSelf();
return false;
}
for (i = 0; i < optionsList.GetLength(); i += 1)
{
if (!nextResult.IsSuccessful()) break;
pickedOptionWithParameters =
AddOptionByCharacter( optionsList.GetCharacter(i), optionsList,
pickedOptionWithParameters)
|| pickedOptionWithParameters;
}
optionsList.FreeSelf();
return nextResult.IsSuccessful();
}
// Assumes `commandParser` and `nextResult` are not `none`.
// Auxiliary method that adds option by it's short version's character
// `optionCharacter`.
// It also accepts `optionSourceList` that describes short option
// expression (e.g. "-rtV") from which it originated for error reporting and
// `forbidOptionWithParameters` that, when set to `true`, forces this method to
// cause the `CET_MultipleOptionsWithParams` error if
// new option has non-empty parameters.
// Method returns `true` if added option had non-empty parameters and
// `false` otherwise.
// Any parsing failure inside this method always causes
// `nextError.DeclareError()` call, so you can use `nextResult.IsSuccessful()`
// to check if method has failed.
private final function bool AddOptionByCharacter(
Text.Character optionCharacter,
Text optionSourceList,
bool forbidOptionWithParameters)
{
local int i;
local bool optionHasParameters;
// Prevent same option appearing twice
for (i = 0; i < usedOptions.length; i += 1)
{
if (_.text.AreEqual(optionCharacter, usedOptions[i].shortName))
{
DeclareError(CET_RepeatedOption, usedOptions[i].longName);
return false;
}
}
// If it's a new option - look it up in all available options
for (i = 0; i < availableOptions.length; i += 1)
{
if (!_.text.AreEqual(optionCharacter, availableOptions[i].shortName)) {
continue;
}
usedOptions[usedOptions.length] = availableOptions[i];
optionHasParameters = (availableOptions[i].required.length > 0
|| availableOptions[i].optional.length > 0);
// Enforce `forbidOptionWithParameters` flag restriction
if (optionHasParameters && forbidOptionWithParameters)
{
DeclareError(CET_MultipleOptionsWithParams, optionSourceList);
return optionHasParameters;
}
// Parse parameters (even if they are empty) and bail
commandParser.Skip();
ParseOptionParameters(availableOptions[i]);
break;
}
if (i >= availableOptions.length) {
DeclareError(CET_UnknownShortOption);
}
return optionHasParameters;
}
// Auxiliary method for parsing option's parameters (including empty ones).
// Automatically fills `nextResult` with parsed parameters
// (or `none` if option has no parameters).
// Assumes `commandParser` and `nextResult` are not `none`.
private final function bool ParseOptionParameters(Command.Option pickedOption)
{
local AssociativeArray optionParameters;
if (currentTargetIsOption && currentTarget != CPT_ExtraParameter) {
DeclareError(CET_NoRequiredParamForOption, targetOption.longName);
return false;
}
if (pickedOption.required.length == 0 && pickedOption.optional.length == 0)
{
nextResult.SetOptionParameters(pickedOption, none);
return true;
}
currentTargetIsOption = true;
targetOption = pickedOption;
optionParameters = ParseParameterArrays( pickedOption.required,
pickedOption.optional);
currentTargetIsOption = false;
if (commandParser.Ok())
{
nextResult.SetOptionParameters(pickedOption, optionParameters);
return true;
}
return false;
}
defaultproperties
{
booleanTrueEquivalents(0) = "true"
booleanTrueEquivalents(1) = "enable"
booleanTrueEquivalents(2) = "on"
booleanTrueEquivalents(3) = "yes"
booleanFalseEquivalents(0) = "false"
booleanFalseEquivalents(1) = "disable"
booleanFalseEquivalents(2) = "off"
booleanFalseEquivalents(3) = "no"
}

158
sources/Commands/Commands.uc

@ -0,0 +1,158 @@
/**
* This feature provides a mechanism to define commands that automatically
* parse their arguments into standard Acedia collection. It also allows to
* manage them (and specify limitation on how they can be called) in a
* centralized manner.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Commands extends Feature
config(AcediaSystem);
// Delimiters that always separate command name from it's parameters
var private array<Text> commandDelimiters;
// Registered commands, recorded as (<command_name>, <command_instance>) pairs
var private AssociativeArray registeredCommands;
// Setting this to `true` enables players to input commands right in the chat
// by prepending them with "!" character.
var public config bool useChatInput;
protected function OnEnabled()
{
registeredCommands = _.collections.EmptyAssociativeArray();
// Macro selector
commandDelimiters[0] = P("@");
// Key selector
commandDelimiters[1] = P("#");
// Player array (possibly JSON array)
commandDelimiters[2] = P("[");
// Negation of the selector
commandDelimiters[3] = P("!");
}
protected function OnDisabled()
{
_.memory.Free(registeredCommands);
registeredCommands = none;
commandDelimiters.length = 0;
}
/**
* Registers given command class, making it available for usage.
*
* If `commandClass` provides command with a name that is already taken
* (comparison is case-insensitive) by a different command - a warning will be
* logged and newly passed `commandClass` discarded.
*
* @param commandClass New command class to register.
*/
public final function RegisterCommand(class<Command> commandClass)
{
local Text commandName;
local Command commandInstance;
if (commandClass == none) return;
if (registeredCommands == none) return;
commandName = commandClass.static.GetName();
commandInstance = Command(registeredCommands.GetItem(commandName));
if (commandInstance != none)
{
_.logger.Failure("Command `" $ string(commandInstance.class)
$ "` with name '" $ commandName.ToPlainString()
$ "' is already registered. Command `" $ string(commandClass)
$ "` will be ignored.");
commandName.FreeSelf();
return;
}
commandInstance = Command(_.memory.Allocate(commandClass, true));
// `commandName` used as a key, do not deallocate it
registeredCommands.SetItem(commandName, commandInstance, true);
}
/**
* Returns command based on a given name.
*
* @param commandName Name of the registered `Command` to return.
* Case-insensitive.
* @return Command, registered with a given name `commandName`.
* If no command with such name was registered - returns `none`.
*/
public final function Command GetCommand(Text commandName)
{
local Text commandNameLowerCase;
local Command commandInstance;
if (commandName == none) return none;
if (registeredCommands == none) return none;
commandNameLowerCase = commandName.LowerCopy();
commandInstance = Command(registeredCommands.GetItem(commandNameLowerCase));
commandNameLowerCase.FreeSelf();
return commandInstance;
}
/**
* Returns array of names of all available commands.
*
* @return Array of names of all available (registered) commands.
*/
public final function array<Text> GetCommandNames()
{
local int i;
local array<AcediaObject> keys;
local Text nextKeyAsText;
local array<Text> keysAsText;
if (registeredCommands == none) return keysAsText;
keys = registeredCommands.GetKeys();
for (i = 0; i < keys.length; i += 1)
{
nextKeyAsText = Text(keys[i]);
if (nextKeyAsText != none) {
keysAsText[keysAsText.length] = nextKeyAsText.Copy();
}
}
return keysAsText;
}
/**
* Handles user input: finds appropriate command and passes the rest of
* the arguments to it for further processing.
*
* @param parser Parser filled with user input that is expected to
* contain command's name and it's parameters.
* @param callerPlayer Player that caused this command call.
*/
public final function HandleInput(Parser parser, APlayer callerPlayer)
{
local Command commandInstance;
local MutableText commandName;
if (parser == none) return;
if (!parser.Ok()) return;
parser.MUntilMany(commandName, commandDelimiters, true, true);
commandInstance = GetCommand(commandName);
commandName.FreeSelf();
if (parser.Ok() && commandInstance != none) {
commandInstance.ProcessInput(parser, callerPlayer).FreeSelf();
}
}
defaultproperties
{
useChatInput = true
requiredListeners(0) = class'BroadcastListener_Commands'
}

482
sources/Commands/PlayersParser.uc

@ -0,0 +1,482 @@
/**
* Object for parsing what converting textual description of a group of
* players into array of `APlayer`s. Depends on the game context.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class PlayersParser extends AcediaObject
dependson(Parser);
/**
* This parser is supposed to parse player set definitions as they
* are used in commands.
* Basic use is to specify one of the selectors:
* 1. Key selector: "#<integer>" (examples: "#1", "#5").
* This one is used to specify players by their key, assigned to
* them when they enter the game. This type of selectors can be used
* when players have hard to type names.
* 2. Macro selector: "@self", "@admin" or just "@".
* "@" and "@self" are identical and can be used to specify player
* that called the command.
* "@admin" can be used to specify all admins in the game at once.
* In future it is planned to make macros extendable by allowing to
* bind more names to specific groups of players.
* 3. Name selectors: quoted strings and any other types of string that
* do not start with either "#" or "@".
* These specify name prefixes: any player with specified prefix
* will be considered to match such selector.
*
* Negated selectors: "!<selector>". Specifying "!" in front of selector
* will select all players that do not match it instead.
*
* Grouped selectors: "['<selector1>', '<selector2>', ... '<selectorN>']".
* Specified selectors are process in order: from left to right.
* First selector works as usual and selects a set of players.
* All the following selectors either
* expand that list (additive ones, without "!" prefix)
* or remove specific players from the list (the ones with "!" prefix).
* Examples of that:
* *. "[@admin, !@self]" - selects all admins, except the one who called
* the command (whether he is admin or not).
* *. "[dkanus, 'mate']" - will select players "dkanus" and "mate".
* Order also matters, since:
* *. "[@admin, !@admin]" - won't select anyone, since it will first
* add all the admins and then remove them.
* *. "[!@admin, @admin]" - will select everyone, since it will first
* select everyone who is not an admin and then adds everyone else.
*/
// Player for which "@" and "@self" macros will refer
var private APlayer selfPlayer;
// Copy of the list of current players at the moment of allocation of
// this `PlayersParser`.
var private array<APlayer> playersSnapshot;
// Players, selected according to selectors we have parsed so far
var private array<APlayer> currentSelection;
// Have we parsed our first selector?
// We need this to know whether to start with the list of
// all players (if first selector removes them) or
// with empty list (if first selector adds them).
var private bool parsedFirstSelector;
// Will be equal to a single-element array [","], used for parsing
var private array<Text> selectorDelimiters;
var const int TSELF, TADMIN, TNOT, TKEY, TMACRO, TCOMMA;
var const int TOPEN_BRACKET, TCLOSE_BRACKET;
protected function Finalizer()
{
selfPlayer = none;
parsedFirstSelector = false;
playersSnapshot.length = 0;
currentSelection.length = 0;
}
/**
* Set a player who will be referred to by "@" and "@self" macros.
*
* @param newSelfPlayer Player who will be referred to by "@" and
* "@self" macros. Passing `none` will make it so no one is
* referred by them.
*/
public final function SetSelf(APLayer newSelfPlayer)
{
selfPlayer = newSelfPlayer;
}
// Insert a new player into currently selected list of players
// (`currentSelection`) such that there will be no duplicates.
// `none` values are auto-discarded.
private final function InsertPlayer(APLayer toInsert)
{
local int i;
if (toInsert == none) {
return;
}
for (i = 0; i < currentSelection.length; i += 1)
{
if (currentSelection[i] == toInsert) {
return;
}
}
currentSelection[currentSelection.length] = toInsert;
}
// Adds all the players with specified key (`key`) to the current selection.
private final function AddByKey(int key)
{
local int i;
for (i = 0; i < playersSnapshot.length; i += 1)
{
if (playersSnapshot[i].GetIdentity().GetKey() == key) {
InsertPlayer(playersSnapshot[i]);
}
}
}
// Removes all the players with specified key (`key`) from
// the current selection.
private final function RemoveByKey(int key)
{
local int i;
while (i < currentSelection.length)
{
if (currentSelection[i].GetIdentity().GetKey() == key) {
currentSelection.Remove(i, 1);
}
else {
i += 1;
}
}
}
// Adds all the players with specified name (`name`) to the current selection.
private final function AddByName(Text name)
{
local int i;
local Text nextPlayerName;
if (name == none) return;
for (i = 0; i < playersSnapshot.length; i += 1)
{
nextPlayerName = playersSnapshot[i].GetName();
if (nextPlayerName.StartsWith(name, SCASE_INSENSITIVE)) {
InsertPlayer(playersSnapshot[i]);
}
nextPlayerName.FreeSelf();
}
}
// Removes all the players with specified name (`name`) from
// the current selection.
private final function RemoveByName(Text name)
{
local int i;
local Text nextPlayerName;
while (i < currentSelection.length)
{
nextPlayerName = currentSelection[i].GetName();
if (nextPlayerName.StartsWith(name, SCASE_INSENSITIVE)) {
currentSelection.Remove(i, 1);
}
else {
i += 1;
}
nextPlayerName.FreeSelf();
}
}
// Adds all the admins to the current selection.
private final function AddAdmins()
{
local int i;
for (i = 0; i < playersSnapshot.length; i += 1)
{
if (playersSnapshot[i].IsAdmin()) {
InsertPlayer(playersSnapshot[i]);
}
}
}
// Removes all the admins from the current selection.
private final function RemoveAdmins()
{
local int i;
while (i < currentSelection.length)
{
if (currentSelection[i].IsAdmin()) {
currentSelection.Remove(i, 1);
}
else {
i += 1;
}
}
}
// Add all the players specified by `macroText` (from macro "@<macroText>").
// Does nothing if there is no such macro.
private final function AddByMacro(Text macroText)
{
if (macroText.Compare(T(TADMIN), SCASE_INSENSITIVE)) {
AddAdmins();
return;
}
if (macroText.IsEmpty() || macroText.Compare(T(TSELF), SCASE_INSENSITIVE)) {
InsertPlayer(selfPlayer);
}
}
// Removes all the players specified by `macroText`
// (from macro "@<macroText>").
// Does nothing if there is no such macro.
private final function RemoveByMacro(Text macroText)
{
local int i;
if (macroText.Compare(T(TADMIN), SCASE_INSENSITIVE)) {
RemoveAdmins();
}
if (macroText.IsEmpty() || macroText.Compare(T(TSELF), SCASE_INSENSITIVE))
{
while (i < currentSelection.length)
{
if (currentSelection[i] == selfPlayer) {
currentSelection.Remove(i, 1);
}
else {
i += 1;
}
}
}
}
// Parses one selector from `parser`, while accordingly modifying current
// player selection list.
private final function ParseSelector(Parser parser)
{
local bool additiveSelector;
local Parser.ParserState confirmedState;
if (parser == none) return;
if (!parser.Ok()) return;
confirmedState = parser.GetCurrentState();
if (!parser.Match(T(TNOT)).Ok())
{
additiveSelector = true;
parser.RestoreState(confirmedState);
}
// Determine whether we stars with empty or full player list
if (!parsedFirstSelector)
{
parsedFirstSelector = true;
if (additiveSelector) {
currentSelection.length = 0;
}
else {
currentSelection = playersSnapshot;
}
}
// Try all selector types
confirmedState = parser.GetCurrentState();
if (parser.Match(T(TKEY)).Ok())
{
ParseKeySelector(parser, additiveSelector);
return;
}
parser.RestoreState(confirmedState);
if (parser.Match(T(TMACRO)).Ok())
{
ParseMacroSelector(parser, additiveSelector);
return;
}
parser.RestoreState(confirmedState);
ParseNameSelector(parser, additiveSelector);
}
// Parse key selector (assuming "#" is already consumed), while accordingly
// modifying current player selection list.
private final function ParseKeySelector(Parser parser, bool additiveSelector)
{
local int key;
if (parser == none) return;
if (!parser.Ok()) return;
if (!parser.MInteger(key).Ok()) return;
if (additiveSelector) {
AddByKey(key);
}
else {
RemoveByKey(key);
}
}
// Parse macro selector (assuming "@" is already consumed), while accordingly
// modifying current player selection list.
private final function ParseMacroSelector(Parser parser, bool additiveSelector)
{
local MutableText macroName;
local Parser.ParserState confirmedState;
if (parser == none) return;
if (!parser.Ok()) return;
confirmedState = parser.GetCurrentState();
macroName = ParseLiteral(parser);
if (!parser.Ok())
{
_.memory.Free(macroName);
return;
}
if (additiveSelector) {
AddByMacro(macroName);
}
else {
RemoveByMacro(macroName);
}
_.memory.Free(macroName);
}
// Parse name selector, while accordingly modifying current player
// selection list.
private final function ParseNameSelector(Parser parser, bool additiveSelector)
{
local MutableText playerName;
local Parser.ParserState confirmedState;
if (parser == none) return;
if (!parser.Ok()) return;
confirmedState = parser.GetCurrentState();
playerName = ParseLiteral(parser);
if (!parser.Ok())
{
_.memory.Free(playerName);
return;
}
if (additiveSelector) {
AddByName(playerName);
}
else {
RemoveByName(playerName);
}
_.memory.Free(playerName);
}
// Reads a string that can either be a body of name selector
// (some player's name prefix) or of a macro selector (what comes after "@").
// This is different from `parser.MString()` because it also uses
// "," as a separator.
private final function MutableText ParseLiteral(Parser parser)
{
local MutableText literal;
local Parser.ParserState confirmedState;
if (parser == none) return none;
if (!parser.Ok()) return none;
confirmedState = parser.GetCurrentState();
if (!parser.MStringLiteral(literal).Ok())
{
parser.RestoreState(confirmedState);
parser.MUntilMany(literal, selectorDelimiters, true);
}
return literal;
}
/**
* Returns players parsed by the last `ParseWith()` or `Parse()` call.
* If neither were yet called - returns an empty array.
*
* @return players parsed by the last `ParseWith()` or `Parse()` call.
*/
public final function array<APlayer> GetPlayers()
{
return currentSelection;
}
/**
* Parses players from `parser` according to the currently present players.
*
* Array of parsed players can be retrieved by `self.GetPlayers()` method.
*
* @param parser `Parser` from which to parse player list.
* It's state will be set to failed in case the parsing fails.
* @return `true` if parsing was successful and `false` otherwise.
*/
public final function bool ParseWith(Parser parser)
{
local Parser.ParserState confirmedState;
if (parser == none) return false;
if (!parser.Ok()) return false;
Reset();
confirmedState = parser.Skip().GetCurrentState();
if (!parser.Match(T(TOPEN_BRACKET)).Ok())
{
ParseSelector(parser.RestoreState(confirmedState));
if (parser.Ok()) {
return true;
}
return false;
}
while (parser.Ok() && !parser.HasFinished())
{
confirmedState = parser.Skip().GetCurrentState();
if (parser.Match(T(TCLOSE_BRACKET)).Ok()) {
return true;
}
parser.RestoreState(confirmedState);
if (parsedFirstSelector) {
parser.Match(T(TCOMMA)).Skip();
}
ParseSelector(parser);
parser.Skip();
}
parser.Fail();
return false;
}
// Resets this object to initial state before parsing and update
// `playersSnapshot` to contain current players.
private final function Reset()
{
local PlayerService service;
parsedFirstSelector = false;
playersSnapshot.length = 0;
currentSelection.length = 0;
service = PlayerService(class'PlayerService'.static.Require());
if (service != none) {
playersSnapshot = service.GetAllPlayers();
}
selectorDelimiters.length = 0;
selectorDelimiters[0] = T(TCOMMA);
selectorDelimiters[1] = T(TCLOSE_BRACKET);
}
/**
* Parses players from `toParse` according to the currently present players.
*
* Array of parsed players can be retrieved by `self.GetPlayers()` method.
*
* @param toParse `Text` from which to parse player list.
* @return `true` if parsing was successful and `false` otherwise.
*/
public final function bool Parse(Text toParse)
{
local bool wasSuccessful;
local Parser parser;
if (toParse == none) {
return false;
}
parser = _.text.Parse(toParse);
wasSuccessful = ParseWith(parser);
parser.FreeSelf();
return wasSuccessful;
}
defaultproperties
{
TSELF = 0
stringConstants(0) = "self"
TADMIN = 1
stringConstants(1) = "admin"
TNOT = 2
stringConstants(2) = "!"
TKEY = 3
stringConstants(3) = "#"
TMACRO = 4
stringConstants(4) = "@"
TCOMMA = 5
stringConstants(5) = ","
TOPEN_BRACKET = 6
stringConstants(6) = "["
TCLOSE_BRACKET = 7
stringConstants(7) = "]"
}

38
sources/Commands/Tests/MockCommandA.uc

@ -0,0 +1,38 @@
/**
* Mock command class for testing.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MockCommandA extends Command;
protected function BuildData(CommandDataBuilder builder)
{
builder.ParamObject(P("just_obj"))
.ParamArrayList(P("manyLists"))
.OptionalParams()
.ParamObject(P("last_obj"));
builder.SubCommand(P("simple"))
.ParamBooleanList(P("isItSimple?"))
.ParamInteger(P("integer variable"), P("int"))
.OptionalParams()
.ParamNumberList(P("numeric list"), P("list"))
.ParamTextList(P("another list"));
}
defaultproperties
{
}

48
sources/Commands/Tests/MockCommandB.uc

@ -0,0 +1,48 @@
/**
* Mock command class for testing.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MockCommandB extends Command;
protected function BuildData(CommandDataBuilder builder)
{
builder.ParamArray(P("just_array"))
.ParamText(P("just_text"));
builder.Option(P("values"))
.ParamIntegerList(P("types"));
builder.Option(P("long"))
.ParamInteger(P("num"))
.ParamNumberList(P("text"))
.ParamBoolean(P("huh"));
builder.Option(P("type"), P("t"))
.ParamText(P("type"));
builder.Option(P("Test"))
.ParamText(P("to_test"));
builder.Option(P("silent"))
.Option(P("forced"))
.Option(P("verbose"), P("V"))
.Option(P("actual"));
builder.SubCommand(P("do"))
.OptionalParams()
.ParamNumberList(P("numeric list"), P("list"))
.ParamBoolean(P("maybe"));
}
defaultproperties
{
}

407
sources/Commands/Tests/TEST_Command.uc

@ -0,0 +1,407 @@
/**
* Set of tests for `Command` class.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_Command extends TestCase
abstract;
var string queryASuccess1, queryASuccess2, queryASuccess3, queryASuccess4;
var string queryAFailure1, queryAFailure2;
var string queryBSuccess1, queryBSuccess2;
var string queryBFailure1, queryBFailure2, queryBFailure3;
var string queryBFailureUnknownOptionLong, queryBFailureUnknownOptionShort;
var string queryBFailureUnused;
var string queryBFailureNoReqParamOption1, queryBFailureNoReqParamOption2;
protected static function Parser PRS(string source)
{
return __().text.ParseString(source);
}
protected static function TESTS()
{
Context("Testing `Command` parsing (parameter chains).");
Test_MockA();
Context("Testing `Command` parsing (options).");
Test_MockB();
Context("Testing `CommandCall` error messages.");
Test_CommandCallErrors();
Context("Testing sub-command determination.");
Test_SubCommandName();
}
protected static function Test_MockA()
{
SubTest_MockAQ1AndFailed();
SubTest_MockAQ2();
SubTest_MockAQ3();
SubTest_MockAQ4();
}
protected static function Test_MockB()
{
SubTest_MockBFailed();
SubTest_MockBQ1();
SubTest_MockBQ2();
}
protected static function Test_CommandCallErrors()
{
SubTest_CommandCallErrorBadParser();
SubTest_CommandCallErrorNoRequiredParam();
SubTest_CommandCallErrorUnknownOption();
SubTest_CommandCallErrorRepeatedOption();
SubTest_CommandCallErrorMultipleOptionsWithParams();
SubTest_CommandCallErrorUnusedCommandParameters();
SubTest_CommandCallErrorNoRequiredParamForOption();
}
protected static function SubTest_CommandCallErrorBadParser()
{
local CommandCall result;
Issue("`CET_BadParser` errors are incorrectly reported.");
result = class'MockCommandA'.static.GetInstance().ProcessInput(none, none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_BadParser);//
TEST_ExpectNone(result.GetErrorCause());
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(__().text.ParseString("stuff").Fail(), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_BadParser);//
TEST_ExpectNone(result.GetErrorCause());
}
protected static function SubTest_CommandCallErrorNoRequiredParam()
{
local CommandCall result;
Issue("`CET_NoRequiredParam` errors are incorrectly reported.");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryAFailure1), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_NoRequiredParam);
TEST_ExpectTrue( result.GetErrorCause().ToPlainString()
== "integer variable");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryAFailure2), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_NoRequiredParam);
TEST_ExpectTrue( result.GetErrorCause().ToPlainString()
== "isItSimple?");
}
protected static function SubTest_CommandCallErrorUnknownOption()
{
local CommandCall result;
Issue("`CET_UnknownOption` errors are incorrectly reported.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailureUnknownOptionLong), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_UnknownOption);
TEST_ExpectTrue( result.GetErrorCause().ToPlainString()
== "kest");
Issue("`CET_UnknownShortOption` errors are incorrectly reported.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailureUnknownOptionShort), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_UnknownShortOption);
TEST_ExpectNone(result.GetErrorCause());
}
protected static function SubTest_CommandCallErrorRepeatedOption()
{
local CommandCall result;
Issue("`CET_RepeatedOption` errors are incorrectly reported.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailure2), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_RepeatedOption);
TEST_ExpectTrue( result.GetErrorCause().ToPlainString()
== "forced");
}
protected static function SubTest_CommandCallErrorUnusedCommandParameters()
{
local CommandCall result;
Issue("`CET_UnusedCommandParameters` errors are incorrectly reported.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailureUnused), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_UnusedCommandParameters);
TEST_ExpectTrue( result.GetErrorCause().ToPlainString()
== "text -j");
}
protected static function SubTest_CommandCallErrorMultipleOptionsWithParams()
{
local CommandCall result;
Issue("`CET_MultipleOptionsWithParams` errors are incorrectly reported.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailure1), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_MultipleOptionsWithParams);
TEST_ExpectTrue(result.GetErrorCause().ToPlainString() == "tv");
}
protected static function SubTest_CommandCallErrorNoRequiredParamForOption()
{
local CommandCall result;
Issue("`CET_NoRequiredParamForOption` errors are incorrectly reported.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailureNoReqParamOption1), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_NoRequiredParamForOption);
TEST_ExpectTrue(result.GetErrorCause().ToPlainString() == "long");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBFailureNoReqParamOption2), none);
TEST_ExpectFalse(result.IsSuccessful());
TEST_ExpectTrue(result.GetError() == CET_NoRequiredParamForOption);
TEST_ExpectTrue(result.GetErrorCause().ToPlainString() == "values");
}
protected static function Test_SubCommandName()
{
local CommandCall result;
Issue("Cannot determine subcommands.");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryASuccess1), none);
TEST_ExpectTrue(result.GetSubCommand().ToPlainString() == "simple");
Issue("Cannot determine when subcommands are missing.");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryASuccess2), none);
TEST_ExpectTrue(result.GetSubCommand().IsEmpty());
}
protected static function SubTest_MockAQ1AndFailed()
{
local Parser parser;
local Command command;
local DynamicArray paramArray;
local AssociativeArray parameters;
parser = Parser(__().memory.Allocate(class'Parser'));
command = class'MockCommandA'.static.GetInstance();
Issue("Command queries that should fail succeed instead.");
parser.InitializeS(default.queryAFailure1);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
parser.InitializeS(default.queryAFailure2);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
Issue("Cannot parse command queries without optional parameters.");
parameters =
command.ProcessInput(parser.InitializeS(default.queryASuccess1), none)
.GetParameters();
TEST_ExpectTrue(parameters.GetLength() == 2);
paramArray = DynamicArray(parameters.GetItem(P("isItSimple?")));
TEST_ExpectTrue(paramArray.GetLength() == 1);
TEST_ExpectFalse(BoolBox(paramArray.GetItem(0)).Get());
TEST_ExpectTrue(IntBox(parameters.GetItem(P("int"))).Get() == 8);
TEST_ExpectFalse(parameters.HasKey(P("list")));
TEST_ExpectFalse(parameters.HasKey(P("another list")));
}
protected static function SubTest_MockAQ2()
{
local DynamicArray paramArray, subArray;
local AssociativeArray result, subObject;
Issue("Cannot parse command queries without optional parameters.");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryASuccess2), none).GetParameters();
TEST_ExpectTrue(result.GetLength() == 2);
subObject = AssociativeArray(result.GetItem(P("just_obj")));
TEST_ExpectTrue(IntBox(subObject.GetItem(P("var"))).Get() == 7);
TEST_ExpectTrue(subObject.HasKey(P("another")));
TEST_ExpectNone(subObject.GetItem(P("another")));
paramArray = DynamicArray(result.GetItem(P("manyLists")));
TEST_ExpectTrue(paramArray.GetLength() == 4);
subArray = DynamicArray(paramArray.GetItem(0));
TEST_ExpectTrue(subArray.GetLength() == 2);
TEST_ExpectTrue(IntBox(subArray.GetItem(0)).Get() == 1);
TEST_ExpectTrue(IntBox(subArray.GetItem(1)).Get() == 2);
subArray = DynamicArray(paramArray.GetItem(1));
TEST_ExpectTrue(subArray.GetLength() == 1);
TEST_ExpectTrue(IntBox(subArray.GetItem(0)).Get() == 3);
TEST_ExpectTrue(DynamicArray(paramArray.GetItem(2)).GetLength() == 0);
subArray = DynamicArray(paramArray.GetItem(3));
TEST_ExpectTrue(subArray.GetLength() == 3);
TEST_ExpectTrue(IntBox(subArray.GetItem(0)).Get() == 8);
TEST_ExpectTrue(IntBox(subArray.GetItem(1)).Get() == 5);
TEST_ExpectTrue(IntBox(subArray.GetItem(2)).Get() == 0);
TEST_ExpectFalse(result.HasKey(P("last_obj")));
}
protected static function SubTest_MockAQ3()
{
local DynamicArray paramArray;
local AssociativeArray result;
Issue("Cannot parse command queries with optional parameters.");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryASuccess3), none).GetParameters();
// Booleans
paramArray = DynamicArray(result.GetItem(P("isItSimple?")));
TEST_ExpectTrue(paramArray.GetLength() == 7);
TEST_ExpectTrue(BoolBox(paramArray.GetItem(0)).Get());
TEST_ExpectFalse(BoolBox(paramArray.GetItem(1)).Get());
TEST_ExpectTrue(BoolBox(paramArray.GetItem(2)).Get());
TEST_ExpectTrue(BoolBox(paramArray.GetItem(3)).Get());
TEST_ExpectFalse(BoolBox(paramArray.GetItem(4)).Get());
TEST_ExpectTrue(BoolBox(paramArray.GetItem(5)).Get());
TEST_ExpectFalse(BoolBox(paramArray.GetItem(6)).Get());
// Integer
TEST_ExpectTrue(IntBox(result.GetItem(P("int"))).Get() == -32);
// Floats
paramArray = DynamicArray(result.GetItem(P("list")));
TEST_ExpectTrue(paramArray.GetLength() == 3);
TEST_ExpectTrue(FloatBox(paramArray.GetItem(0)).Get() == 0.45);
TEST_ExpectTrue(FloatBox(paramArray.GetItem(1)).Get() == 234.7);
TEST_ExpectTrue(FloatBox(paramArray.GetItem(2)).Get() == 13);
// `Text`s
paramArray = DynamicArray(result.GetItem(P("another list")));
TEST_ExpectTrue(paramArray.GetLength() == 3);
TEST_ExpectTrue(Text(paramArray.GetItem(0)).ToPlainString() == "dk");
TEST_ExpectTrue(Text(paramArray.GetItem(1)).ToPlainString() == "someone");
TEST_ExpectTrue( Text(paramArray.GetItem(2)).ToFormattedString()
== "complex {rgb(123,45,72) string}");
}
protected static function SubTest_MockAQ4()
{
local DynamicArray paramArray;
local AssociativeArray result, subObject;
Issue("Cannot parse command queries with optional parameters.");
result = class'MockCommandA'.static.GetInstance()
.ProcessInput(PRS(default.queryASuccess4), none).GetParameters();
TEST_ExpectTrue(result.GetLength() == 3);
subObject = AssociativeArray(result.GetItem(P("just_obj")));
TEST_ExpectTrue(IntBox(subObject.GetItem(P("var"))).Get() == 7);
TEST_ExpectTrue(subObject.HasKey(P("another")));
TEST_ExpectNone(subObject.GetItem(P("another")));
paramArray = DynamicArray(result.GetItem(P("manyLists")));
TEST_ExpectTrue(paramArray.GetLength() == 4);
subObject = AssociativeArray(result.GetItem(P("last_obj")));
TEST_ExpectTrue(subObject.GetLength() == 0);
}
protected static function SubTest_MockBFailed()
{
local Parser parser;
local Command command;
parser = Parser(__().memory.Allocate(class'Parser'));
command = class'MockCommandB'.static.GetInstance();
Issue("Command queries that should fail succeed instead.");
parser.InitializeS(default.queryBFailure1);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
parser.InitializeS(default.queryBFailure2);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
parser.InitializeS(default.queryBFailure3);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
parser.InitializeS(default.queryBFailureNoReqParamOption1);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
parser.InitializeS(default.queryBFailureNoReqParamOption2);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
parser.InitializeS(default.queryBFailureUnknownOptionLong);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
parser.InitializeS(default.queryBFailureUnknownOptionShort);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
parser.InitializeS(default.queryBFailureUnused);
TEST_ExpectFalse(command.ProcessInput(parser, none).IsSuccessful());
}
protected static function SubTest_MockBQ1()
{
local CommandCall result;
local DynamicArray subArray;
local AssociativeArray params, options, subObject;
Issue("Cannot parse command queries with options.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBSuccess1), none);
params = result.GetParameters();
TEST_ExpectTrue(params.GetLength() == 2);
subArray = DynamicArray(params.GetItem(P("just_array")));
TEST_ExpectTrue(subArray.GetLength() == 2);
TEST_ExpectTrue(IntBox(subArray.GetItem(0)).Get() == 7);
TEST_ExpectNone(subArray.GetItem(1));
TEST_ExpectTrue( Text(params.GetItem(P("just_text"))).ToPlainString()
== "text");
options = result.GetOptions();
TEST_ExpectTrue(options.GetLength() == 1);
subObject = AssociativeArray(options.GetItem(P("values")));
TEST_ExpectTrue(subObject.GetLength() == 1);
subArray = DynamicArray(subObject.GetItem(P("types")));
TEST_ExpectTrue(subArray.GetLength() == 5);
TEST_ExpectTrue(IntBox(subArray.GetItem(0)).Get() == 1);
TEST_ExpectTrue(IntBox(subArray.GetItem(1)).Get() == 3);
TEST_ExpectTrue(IntBox(subArray.GetItem(2)).Get() == 5);
TEST_ExpectTrue(IntBox(subArray.GetItem(3)).Get() == 2);
TEST_ExpectTrue(IntBox(subArray.GetItem(4)).Get() == 4);
}
protected static function SubTest_MockBQ2()
{
local CommandCall result;
local DynamicArray subArray;
local AssociativeArray options, subObject;
Issue("Cannot parse command queries with mixed-in options.");
result = class'MockCommandB'.static.GetInstance()
.ProcessInput(PRS(default.queryBSuccess2), none);
TEST_ExpectTrue(result.GetParameters().GetLength() == 0);
options = result.GetOptions();
TEST_ExpectTrue(options.GetLength() == 7);
TEST_ExpectTrue(options.HasKey(P("actual")));
TEST_ExpectNone(options.GetItem(P("actual")));
TEST_ExpectTrue(options.HasKey(P("silent")));
TEST_ExpectNone(options.GetItem(P("silent")));
TEST_ExpectTrue(options.HasKey(P("verbose")));
TEST_ExpectNone(options.GetItem(P("verbose")));
TEST_ExpectTrue(options.HasKey(P("forced")));
TEST_ExpectNone(options.GetItem(P("forced")));
subObject = AssociativeArray(options.GetItem(P("type")));
TEST_ExpectTrue( Text(subObject.GetItem(P("type"))).ToPlainString()
== "value");
subObject = AssociativeArray(options.GetItem(P("Test")));
TEST_ExpectTrue(Text(subObject.GetItem(P("to_test"))).IsEmpty());
subObject = AssociativeArray(options.GetItem(P("values")));
subArray = DynamicArray(subObject.GetItem(P("types")));
TEST_ExpectTrue(subArray.GetLength() == 1);
TEST_ExpectTrue(IntBox(subArray.GetItem(0)).Get() == 8);
}
defaultproperties
{
caseName = "Command"
caseGroup = "Commands"
queryASuccess1 = "simple disable 0o10 "
queryASuccess2 = "{\"var\": 7, \"another\": null} [1,2] [3] [] [8, 5, 0]"
queryASuccess3 = "simple true false enable yes no on off -32 45e-2 234.7 13 dk someone \"complex {#7b2d48 string}\" "
queryASuccess4 = "{\"var\": 7, \"another\": null} [1,2] [3] [] [8, 5, 0] {}"
queryAFailure1 = "simple true false enable yes no no on disable yes off false"
queryAFailure2 = "simple fal"
queryBSuccess1 = "[7, null] --values 1 3 5 2 4 text"
queryBSuccess2 = "do --type \"value\" -va 8 -sV --forced -T"
// long then same as short
queryBFailure1 = "[] 8 -tv 13"
queryBFailure2 = "do 7 5 -sfV --forced yes"
queryBFailure3 = "[] 8 -l 12 14 23.3 3.71 -t `something` -7e4 false text"
queryBFailureNoReqParamOption1 = "[] 8 --long 12 14 23.3 3.71 --type `something` -7e4 false text"
queryBFailureNoReqParamOption2 = "[] 8 -v"
queryBFailureUnknownOptionLong = "[] text --kest"
queryBFailureUnknownOptionShort = "[] text -j"
queryBFailureUnused = "[] 8 text -j"
}

252
sources/Commands/Tests/TEST_CommandDataBuilder.uc

@ -0,0 +1,252 @@
/**
* Set of tests for `CommandDataBuilder` class.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_CommandDataBuilder extends TestCase
dependson(CommandDataBuilder)
abstract;
protected static function CommandDataBuilder PrepareBuilder()
{
local CommandDataBuilder builder;
builder =
CommandDataBuilder(__().memory.Allocate(class'CommandDataBuilder'));
builder.ParamNumber(P("var")).ParamText(P("str_var"), P("otherName"));
builder.OptionalParams();
builder.Describe(P("Simple command"));
builder.ParamBooleanList(P("list"), PBF_OnOff);
// Subcommands
builder.SubCommand(P("sub")).ParamArray(P("array_var"));
builder.Describe(P("Alternative command!"));
builder.ParamIntegerList(P("int"));
builder.SubCommand(P("empty"));
builder.Describe(P("Empty one!"));
builder.SubCommand(P("huh")).ParamNumber(P("list"));
builder.SubCommand(P("sub")).ParamObjectList(P("one_more"), P("but"));
builder.Describe(P("Alternative command! Updated!"));
// Options
builder.Option(P("silent")).Describe(P("Just an option, I dunno."));
builder.Option(P("Params"), P("d"));
builder.ParamBoolean(P("www"), PBF_YesNo, P("random"));
builder.OptionalParams().ParamIntegerList(P("www2"));
return builder.RequireTarget();
}
protected static function Command.SubCommand GetSubCommand(
Command.Data data,
string subCommandName)
{
local int i;
local Command.SubCommand emptySubCommand;
for (i = 0; i < data.subcommands.length; i += 1)
{
if (data.subcommands[i].name.CompareToPlainString(subCommandName)) {
return data.subcommands[i];
}
}
return emptySubCommand;
}
protected static function Command.Option GetOption(
Command.Data data,
string subCommandName)
{
local int i;
local Command.Option emptyOption;
for (i = 0; i < data.options.length; i += 1)
{
if (data.options[i].longName.CompareToPlainString(subCommandName)) {
return data.options[i];
}
}
return emptyOption;
}
protected static function TESTS()
{
Test_Empty();
Test_Full();
}
protected static function Test_Empty()
{
local Command.Data data;
local CommandDataBuilder builder;
Context("Testing that new `CommandDataBuilder` returns"
@ "blank command data.");
builder =
CommandDataBuilder(__().memory.Allocate(class'CommandDataBuilder'));
data = builder.GetData();
TEST_ExpectTrue(data.subcommands.length == 1);
TEST_ExpectTrue(data.subcommands[0].name.IsEmpty());
TEST_ExpectNone(data.subcommands[0].description);
TEST_ExpectTrue(data.subcommands[0].required.length == 0);
TEST_ExpectTrue(data.subcommands[0].optional.length == 0);
TEST_ExpectTrue(data.options.length == 0);
TEST_ExpectFalse(data.requiresTarget);
}
protected static function Test_Full()
{
local Command.Data data;
data = PrepareBuilder().GetData();
Context("Testing that `CommandDataBuilder` properly builds command data for"
@ "complex commands.");
Issue("Incorrect amount of sub-commands and/or option.");
TEST_ExpectTrue(data.subcommands.length == 4);
TEST_ExpectTrue(data.options.length == 2);
TEST_ExpectTrue(data.requiresTarget);
// Test empty sub command.
Issue("\"empty\" command was filled incorrectly.");
TEST_ExpectTrue( GetSubCommand(data, "empty").name.ToPlainString()
== "empty");
TEST_ExpectTrue( GetSubCommand(data, "empty").description.ToPlainString()
== "Empty one!");
TEST_ExpectTrue(GetSubCommand(data, "empty").required.length == 0);
TEST_ExpectTrue(GetSubCommand(data, "empty").optional.length == 0);
// Sub other commands / options
SubTest_DefaultSubCommand(data);
SubTest_subSubCommand(data);
SubTest_huhSubCommand(data);
SubTest_silentOption(data);
SubTest_ParamsOption(data);
}
protected static function SubTest_DefaultSubCommand(Command.Data data)
{
local Command.SubCommand subCommand;
Issue("Default sub-command was filled incorrectly.");
subCommand = GetSubCommand(data, "");
TEST_ExpectTrue(subCommand.name.IsEmpty());
TEST_ExpectTrue(subCommand.description.ToPlainString() == "Simple command");
TEST_ExpectTrue(subCommand.required.length == 2);
TEST_ExpectTrue(subCommand.optional.length == 1);
// Required
TEST_ExpectTrue( subCommand.required[0].displayName.ToPlainString()
== "var");
TEST_ExpectTrue( subCommand.required[0].variableName.ToPlainString()
== "var");
TEST_ExpectTrue(subCommand.required[0].type == CPT_Number);
TEST_ExpectFalse(subCommand.required[0].allowsList);
TEST_ExpectTrue( subCommand.required[1].displayName.ToPlainString()
== "str_var");
TEST_ExpectTrue( subCommand.required[1].variableName.ToPlainString()
== "otherName");
TEST_ExpectTrue(subCommand.required[1].type == CPT_Text);
TEST_ExpectFalse(subCommand.required[1].allowsList);
// Optional
TEST_ExpectTrue( subCommand.optional[0].displayName.ToPlainString()
== "list");
TEST_ExpectTrue( subCommand.optional[0].variableName.ToPlainString()
== "list");
TEST_ExpectTrue(subCommand.optional[0].type == CPT_Boolean);
TEST_ExpectTrue(subCommand.optional[0].booleanFormat == PBF_OnOff);
TEST_ExpectTrue(subCommand.optional[0].allowsList);
}
protected static function SubTest_subSubCommand(Command.Data data)
{
local Command.SubCommand subCommand;
Issue("\"sub\" sub-command was filled incorrectly.");
subCommand = GetSubCommand(data, "sub");
TEST_ExpectTrue(subCommand.name.ToPlainString() == "sub");
TEST_ExpectTrue( subCommand.description.ToPlainString()
== "Alternative command! Updated!");
TEST_ExpectTrue(subCommand.required.length == 3);
TEST_ExpectTrue(subCommand.optional.length == 0);
// Required
TEST_ExpectTrue( subCommand.required[0].displayName.ToPlainString()
== "array_var");
TEST_ExpectTrue( subCommand.required[0].variableName.ToPlainString()
== "array_var");
TEST_ExpectTrue(subCommand.required[0].type == CPT_Array);
TEST_ExpectFalse(subCommand.required[0].allowsList);
TEST_ExpectTrue( subCommand.required[1].displayName.ToPlainString()
== "int");
TEST_ExpectTrue( subCommand.required[1].variableName.ToPlainString()
== "int");
TEST_ExpectTrue(subCommand.required[1].type == CPT_Integer);
TEST_ExpectTrue(subCommand.required[1].allowsList);
TEST_ExpectTrue( subCommand.required[2].displayName.ToPlainString()
== "one_more");
TEST_ExpectTrue( subCommand.required[2].variableName.ToPlainString()
== "but");
TEST_ExpectTrue(subCommand.required[2].type == CPT_Object);
TEST_ExpectTrue(subCommand.required[2].allowsList);
}
protected static function SubTest_huhSubCommand(Command.Data data)
{
local Command.SubCommand subCommand;
Issue("\"huh\" sub-command was filled incorrectly.");
subCommand = GetSubCommand(data, "huh");
TEST_ExpectTrue(subCommand.name.ToPlainString() == "huh");
TEST_ExpectNone(subCommand.description);
TEST_ExpectTrue(subCommand.required.length == 1);
TEST_ExpectTrue(subCommand.optional.length == 0);
// Required
TEST_ExpectTrue( subCommand.required[0].displayName.ToPlainString()
== "list");
TEST_ExpectTrue( subCommand.required[0].variableName.ToPlainString()
== "list");
TEST_ExpectTrue(subCommand.required[0].type == CPT_Number);
TEST_ExpectFalse(subCommand.required[0].allowsList);
}
protected static function SubTest_silentOption(Command.Data data)
{
local Command.Option option;
Issue("\"silent\" option was filled incorrectly.");
option = GetOption(data, "silent");
TEST_ExpectTrue(option.longName.ToPlainString() == "silent");
TEST_ExpectTrue(option.shortName.codePoint == 0x73); // s
TEST_ExpectTrue( option.description.ToPlainString()
== "Just an option, I dunno.");
TEST_ExpectTrue(option.required.length == 0);
TEST_ExpectTrue(option.optional.length == 0);
}
protected static function SubTest_ParamsOption(Command.Data data)
{
local Command.Option option;
Issue("\"Params\" option was filled incorrectly.");
option = GetOption(data, "Params");
TEST_ExpectTrue(option.longName.ToPlainString() == "Params");
TEST_ExpectTrue(option.shortName.codePoint == 0x64);
TEST_ExpectNone(option.description);
TEST_ExpectTrue(option.required.length == 1);
TEST_ExpectTrue(option.optional.length == 1);
// Required
TEST_ExpectTrue(option.required[0].displayName.ToPlainString() == "www");
TEST_ExpectTrue( option.required[0].variableName.ToPlainString()
== "random");
TEST_ExpectTrue(option.required[0].type == CPT_Boolean);
TEST_ExpectTrue(option.required[0].booleanFormat == PBF_YesNo);
TEST_ExpectFalse(option.required[0].allowsList);
// Optional
TEST_ExpectTrue(option.optional[0].displayName.ToPlainString() == "www2");
TEST_ExpectTrue(option.optional[0].variableName.ToPlainString() == "www2");
TEST_ExpectTrue(option.optional[0].type == CPT_Integer);
TEST_ExpectTrue(option.optional[0].allowsList);
}
defaultproperties
{
caseName = "Command data builder"
caseGroup = "Commands"
}

49
sources/Console/ConsoleAPI.uc

@ -23,7 +23,7 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ConsoleAPI extends Singleton
class ConsoleAPI extends AcediaObject
config(AcediaSystem);
/**
@ -190,45 +190,6 @@ public final function SetDefaultColor(Color newDefaultColor)
defaultColor = newDefaultColor;
}
/**
* Returns borrowed `ConsoleWriter` instance that will write into
* consoles of all players.
*
* @return ConsoleWriter Borrowed `ConsoleWriter` instance, configured to
* write into consoles of all players.
* Never `none`.
*/
public final function ConsoleWriter ForAll()
{
local ConsoleDisplaySettings globalSettings;
globalSettings.defaultColor = defaultColor;
globalSettings.maxTotalLineWidth = maxTotalLineWidth;
globalSettings.maxVisibleLineWidth = maxVisibleLineWidth;
return ConsoleWriter(_.memory.Claim(class'ConsoleWriter'))
.Initialize(globalSettings).ForAll();
}
/**
* Returns borrowed `ConsoleWriter` instance that will write into
* console of the player with a given controller.
*
* @param targetController Player, to whom console we want to write.
* If `none` - returned `ConsoleWriter` would be configured to
* throw messages away.
* @return Borrowed `ConsoleWriter` instance, configured to
* write into consoles of all players.
* Never `none`.
*/
public final function ConsoleWriter For(PlayerController targetController)
{
local ConsoleDisplaySettings globalSettings;
globalSettings.defaultColor = defaultColor;
globalSettings.maxTotalLineWidth = maxTotalLineWidth;
globalSettings.maxVisibleLineWidth = maxVisibleLineWidth;
return ConsoleWriter(_.memory.Claim(class'ConsoleWriter'))
.Initialize(globalSettings).ForController(targetController);
}
/**
* Returns new `ConsoleWriter` instance that will write into
* consoles of all players.
@ -236,9 +197,9 @@ public final function ConsoleWriter For(PlayerController targetController)
*
* @return ConsoleWriter New `ConsoleWriter` instance, configured to
* write into consoles of all players.
* Never `none`.
* Guaranteed to not be `none`.
*/
public final function ConsoleWriter MakeForAll()
public final function ConsoleWriter ForAll()
{
local ConsoleDisplaySettings globalSettings;
globalSettings.defaultColor = defaultColor;
@ -258,9 +219,9 @@ public final function ConsoleWriter MakeForAll()
* throw messages away.
* @return New `ConsoleWriter` instance, configured to
* write into consoles of all players.
* Never `none`.
* Guaranteed to not be `none`.
*/
public final function ConsoleWriter MakeFor(PlayerController targetController)
public final function ConsoleWriter For(PlayerController targetController)
{
local ConsoleDisplaySettings globalSettings;
globalSettings.defaultColor = defaultColor;

90
sources/Console/ConsoleBuffer.uc

@ -28,6 +28,15 @@ class ConsoleBuffer extends AcediaObject
* `ConsoleBuffer` works by breaking it's input into words, counting how much
* space they take up and only then deciding to which line to append them
* (new or the next, new one).
*
* It is implemented making heavier use of `string`s instead of `Text`.
* This is because:
* 1. `string`s that are passed to console are broken into lines,
* that need to be specifically prepared anyway;
* 2. It was coded before I the switch to mostly using `Text`,
* when a lot of methods were also accepting `string`s
* and `array<Character>` types as parameters.
* And i really do not want to have to reimplement it.
*/
var private int CODEPOINT_ESCAPE;
@ -43,7 +52,8 @@ var private ConsoleAPI.ConsoleDisplaySettings displaySettings;
*/
struct LineRecord
{
// Contents of the line, in `STRING_Colored` format
// Contents of the line, as a colored `string`.
// Not a `Text`, because it has to be prepared exactly how we want it.
var string contents;
// Is this a wrapped line?
// `true` means that this line was supposed to be part part of another,
@ -69,6 +79,10 @@ var private LineRecord currentLine;
// Word we are currently building, colors of it's characters will be
// automatically converted into `STRCOLOR_Struct`, according to the default
// color setting at the time of their addition.
// We are using array of `Character`s instead of `MutableText` since
// we want to have a more directly control over how it is converted into
// a colored string anyway and otherwise only need an ability to
// append `Character`s to it.
var private array<Text.Character> wordBuffer;
// Amount of color swaps inside `wordBuffer`
var private int colorSwapsInWordBuffer;
@ -108,7 +122,7 @@ public final function ConsoleBuffer SetSettings(
* "Completed line" means that nothing else will be added to it.
* So negative (`false`) response does not mean that the buffer is empty, -
* it can still contain an uncompleted and non-empty line that can still be
* expanded with `InsertString()`. If you want to completely empty the buffer -
* expanded with `Insert()`. If you want to completely empty the buffer -
* call the `Flush()` method.
* Also see `IsEmpty()`.
*
@ -158,43 +172,44 @@ public final function ConsoleBuffer Clear()
* the line after the `input`, call `Flush()` or add line feed symbol "\n"
* at the end of the `input` if you want that.
*
* @param input `string` to be added to the current line in caller
* @param input `Text` to be added to the current line in caller
* `ConsoleBuffer`.
* @param inputType How to treat given `string` regarding coloring.
* @return Returns caller `ConsoleBuffer` to allow method chaining.
*/
public final function ConsoleBuffer InsertString(
string input,
Text.StringType inputType)
public final function ConsoleBuffer Insert(Text input)
{
local int inputConsumed;
local array<Text.Character> rawInput;
rawInput = _().text.StringToRaw(input, inputType);
while (rawInput.length > 0)
local Text.Character nextCharacter;
// Regular symbols and whitespaces are treated differently when
// breaking input into lines, so alternate between adding them,
// switching the logic appropriately
while (inputConsumed < input.GetLength())
{
// Fill word buffer, remove consumed input from `rawInput`
inputConsumed = 0;
while (inputConsumed < rawInput.length)
while (inputConsumed < input.GetLength())
{
if (_().text.IsWhitespace(rawInput[inputConsumed])) break;
InsertIntoWordBuffer(rawInput[inputConsumed]);
nextCharacter = input.GetCharacter(inputConsumed);
if (_.text.IsWhitespace(nextCharacter)) {
break;
}
InsertIntoWordBuffer(input.GetCharacter(inputConsumed));
inputConsumed += 1;
}
rawInput.Remove(0, inputConsumed);
// If we didn't encounter any whitespace symbols - bail
if (rawInput.length <= 0) {
if (inputConsumed >= input.GetLength()) {
return self;
}
FlushWordBuffer();
// Dump whitespaces into lines
inputConsumed = 0;
while (inputConsumed < rawInput.length)
while (inputConsumed < input.GetLength())
{
if (!_().text.IsWhitespace(rawInput[inputConsumed])) break;
AppendWhitespaceToCurrentLine(rawInput[inputConsumed]);
nextCharacter = input.GetCharacter(inputConsumed);
if (!_.text.IsWhitespace(nextCharacter)) {
break;
}
AppendWhitespaceToCurrentLine(nextCharacter);
inputConsumed += 1;
}
rawInput.Remove(0, inputConsumed);
}
return self;
}
@ -234,19 +249,24 @@ public final function ConsoleBuffer Flush()
private final function InsertIntoWordBuffer(Text.Character newCharacter)
{
local int newCharacterIndex;
local Text.Formatting newFormatting;
local Color oldColor, newColor;
newCharacterIndex = wordBuffer.length;
// Fix text color in the buffer to remember default color, if we use it.
newCharacter.color =
_().text.GetCharacterColor(newCharacter, displaySettings.defaultColor);
newCharacter.colorType = STRCOLOR_Struct;
newFormatting = _.text.GetCharacterFormatting(newCharacter);
newFormatting.color =
_.text.GetCharacterColor(newCharacter, displaySettings.defaultColor);
newFormatting.isColored = true;
newCharacter = _.text.SetFormatting(newCharacter, newFormatting);
// Add new character and check if color swapped
newCharacterIndex = wordBuffer.length;
wordBuffer[newCharacterIndex] = newCharacter;
if (newCharacterIndex <= 0) {
return;
}
oldColor = wordBuffer[newCharacterIndex].color;
newColor = wordBuffer[newCharacterIndex - 1].color;
if (!_().color.AreEqual(oldColor, newColor, true)) {
newColor = newFormatting.color;
oldColor = _.text.GetCharacterColor(wordBuffer[newCharacterIndex - 1]);
if (!_.color.AreEqual(oldColor, newColor, true)) {
colorSwapsInWordBuffer += 1;
}
}
@ -264,10 +284,10 @@ private final function FlushWordBuffer()
if (!CanAppendNonWhitespaceIntoLine(wordBuffer[i])) {
BreakLine(true);
}
newColor = wordBuffer[i].color;
newColor = _.text.GetCharacterColor(wordBuffer[i]);
if (MustSwapColorsFor(newColor))
{
currentLine.contents $= _().color.GetColorTag(newColor);
currentLine.contents $= _.color.GetColorTag(newColor);
currentLine.totalSymbolsStored += COLOR_SEQUENCE_LENGTH;
currentLine.colorInserted = true;
currentLine.endColor = newColor;
@ -293,7 +313,7 @@ private final function BreakLine(bool makeWrapped)
private final function bool MustSwapColorsFor(Color newColor)
{
if (!currentLine.colorInserted) return true;
return !_().color.AreEqual(currentLine.endColor, newColor, true);
return !_.color.AreEqual(currentLine.endColor, newColor, true);
}
private final function bool CanAppendWhitespaceIntoLine()
@ -324,7 +344,7 @@ private final function bool CanAppendNonWhitespaceIntoLine(
if (!CanAppendWhitespaceIntoLine()) {
return false;
}
if (!MustSwapColorsFor(nextCharacter.color)) {
if (!MustSwapColorsFor(_.text.GetCharacterColor(nextCharacter))) {
return true;
}
// Can we fit character + color swap sequence?
@ -336,8 +356,8 @@ private final function bool CanAppendNonWhitespaceIntoLine(
// the burden of checking is on the caller.
private final function AppendWhitespaceToCurrentLine(Text.Character whitespace)
{
if (_().text.IsCodePoint(whitespace, CODEPOINT_NEWLINE)) {
BreakLine(true);
if (_.text.IsCodePoint(whitespace, CODEPOINT_NEWLINE)) {
BreakLine(false);
return;
}
if (!CanAppendWhitespaceIntoLine()) {
@ -378,7 +398,7 @@ private final function bool WordCanFitInCurrentLine()
// Total symbols check
totalCharactersInWord = wordBuffer.length
+ colorSwapsInWordBuffer * COLOR_SEQUENCE_LENGTH;
if (MustSwapColorsFor(wordBuffer[0].color)) {
if (MustSwapColorsFor(_.text.GetCharacterColor(wordBuffer[0]))) {
totalCharactersInWord += COLOR_SEQUENCE_LENGTH;
}
return (totalCharactersInWord <= totalLimit);

62
sources/Console/ConsoleWriter.uc

@ -54,7 +54,7 @@ public final function ConsoleWriter Initialize(
{
displaySettings = newDisplaySettings;
if (outputBuffer == none) {
outputBuffer = ConsoleBuffer(_().memory.Allocate(class'ConsoleBuffer'));
outputBuffer = ConsoleBuffer(_.memory.Allocate(class'ConsoleBuffer'));
}
else {
outputBuffer.Clear();
@ -257,73 +257,29 @@ public final function ConsoleWriter Flush()
}
/**
* Writes a formatted string into console.
* Writes text's contents into console.
*
* Does not trigger console output, for that use `WriteLine()` or `Flush()`.
*
* To output a different type of string into a console, use `WriteT()`.
*
* @param message Formatted string to output.
* @param message `Text` to output.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter Write(string message)
public final function ConsoleWriter Write(Text message)
{
outputBuffer.InsertString(message, STRING_Formatted);
outputBuffer.Insert(message);
return self;
}
/**
* Writes a formatted string into console.
* Writes text's contents into console.
* Result will be output immediately, starts a new line.
*
* To output a different type of string into a console, use `WriteLineT()`.
*
* @param message Formatted string to output.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter WriteLine(string message)
{
outputBuffer.InsertString(message, STRING_Formatted);
Flush();
return self;
}
/**
* Writes a `string` of specified type into console.
*
* Does not trigger console output, for that use `WriteLineT()` or `Flush()`.
*
* To output a formatted string you might want to simply use `Write()`.
*
* @param message String of a given type to output.
* @param inputType Type of the string method should output.
* @param message `Text` to output.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter WriteT(
string message,
Text.StringType inputType)
public final function ConsoleWriter WriteLine(Text message)
{
outputBuffer.InsertString(message, inputType);
return self;
}
/**
* Writes a `string` of specified type into console.
* Result will be output immediately, starts a new line.
*
* To output a formatted string you might want to simply use `WriteLine()`.
*
* @param message String of a given type to output.
* @param inputType Type of the string method should output.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter WriteLineT(
string message,
Text.StringType inputType)
{
outputBuffer.InsertString(message, inputType);
Flush();
return self;
return Write(message).Flush();
}
// Send all completed lines from an `outputBuffer`

29
sources/CoreService.uc

@ -0,0 +1,29 @@
/**
* Core service that is always running alongside Acedia framework, must be
* created by a launcher.
* Does nothing, simply used for spawning `Actor`s.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CoreService extends Service;
defaultproperties
{
// Since `CoreService` is what we use to start spawning `Actor`s,
// we have to allow launcher to spawn it with `Spawn()` call
blockSpawning = false
}

540
sources/Data/Collections/AssociativeArray.uc

@ -0,0 +1,540 @@
/**
* This class implements an associative array for storing arbitrary types
* of data that provides a quick (near constant) access to *values* by
* associated *keys*.
* Since UnrealScript lacks any sort of templating, `AssociativeArray`
* stores generic `AcediaObject` keys and values. `Text` can be used instead of
* typical `string` keys and primitive values can be added in their boxed form
* (either as actual `<Type>Box` or as it's reference counterpart).
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AssociativeArray extends Collection;
// Defines key <-> value (with managed status) mapping.
// Stores lifetime information to ensure that values were not reallocated
// after being added to the collection.
struct Entry
{
var public AcediaObject key;
var protected int keyLifeVersion;
var public AcediaObject value;
var protected int valueLifeVersion;
var public bool managed;
};
// Bucket of entries. Used to store entries with the same index in hash table.
struct Bucket
{
var array<Entry> entries;
};
var private array<Bucket> hashTable;
// Amount of elements currently stored in this `AssociativeArray`.
// If one of the keys was deallocated outside of `AssociativeArray`,
// this value may overestimate actual amount of elements.
var private int storedElementCount;
// Lower and upper limits on hash table capacity.
var private const int MINIMUM_CAPACITY;
var private const int MAXIMUM_CAPACITY;
// Minimum and maximum allowed density of elements
// (`storedElementCount / hashTable.length`).
// If density falls outside this range, - we have to resize hash table to
// get into (MINIMUM_DENSITY; MAXIMUM_DENSITY) bounds, as long as it does not
// violate capacity restrictions.
var private const float MINIMUM_DENSITY;
var private const float MAXIMUM_DENSITY;
/**
* Auxiliary struct, necessary to implement iterator for `AssociativeArray`.
* Can be used for manual iteration, but should be avoided in favor of
* `Iterator`.
*/
struct Index
{
var protected int bucketIndex;
var protected int entryIndex;
};
protected function Constructor()
{
UpdateHashTableCapacity();
}
protected function Finalizer()
{
Empty();
}
// Auxiliary method that is needed as a replacement for `%` module
// operator, since it is an operation on `float`s in UnrealScript and does not
// have appropriate value range to work with hashes.
// Assumes non-negative input.
private function int Remainder(int number, int divisor)
{
local int quotient;
quotient = number / divisor;
return (number - quotient * divisor);
}
// Calculates appropriate bucket index for the given key.
// Assumes that given key is not `none` and is allocated.
private final function int GetBucketIndex(AcediaObject key)
{
local int bucketIndex;
bucketIndex = key.GetHashCode();
if (bucketIndex < 0) {
// Minimum `int` value is greater than maximum one in absolute value,
// so shift it up to avoid overflow.
bucketIndex = -1 * (bucketIndex + 1);
}
bucketIndex = Remainder(bucketIndex, hashTable.length);
return bucketIndex;
}
// Accessing value in `AssociativeArray` requires:
// 1. Two level lookup of both bucket and entry (inside that bucket)
// indices;
// 2. Lifetime checks to ensure no-one reallocated keys/values we
// are using;
// 3. Appropriate clean up o keys/values that were already deallocated.
//
// We spread the cost of the cleaning by pairing it with every bucket
// access.
// We only clean one (accessed) bucket per `FindEntryIndices()` and,
// given that there isn't many hash collisions, this operation should not be
// noticeably expensive.
//
// As a result returns bucket's and entry's indices in `bucketIndex` and
// `entryIndex` out variables.
// `bucketIndex` is guaranteed to be found for non-`none` keys,
// `entryIndex` is valid iff method returns `true`, otherwise it's equal to
// the index at which new property can get inserted.
private final function bool FindEntryIndices(
AcediaObject key,
out int bucketIndex,
out int entryIndex)
{
local int i;
local array<Entry> bucketEntries;
if (key == none) return false;
if (!key.IsAllocated()) return false;
bucketIndex = GetBucketIndex(key);
CleanBucket(hashTable[bucketIndex]);
// Check if bucket actually has given key.
bucketEntries = hashTable[bucketIndex].entries;
for (i = 0; i < bucketEntries.length; i += 1)
{
if (key.IsEqual(bucketEntries[i].key))
{
entryIndex = i;
return true;
}
}
entryIndex = bucketEntries.length;
return false;
}
// Cleans given bucket from entries with deallocated/reallocated
// keys or values.
private final function CleanBucket(out Bucket bucketToClean)
{
local int i;
local Entry nextEntry;
local array<Entry> bucketEntries;
bucketEntries = bucketToClean.entries;
i = 0;
while (i < bucketEntries.length)
{
nextEntry = bucketEntries[i];
// If value was already reallocated - set it to `none`.
if ( nextEntry.value != none
&& nextEntry.value.GetLifeVersion() != nextEntry.valueLifeVersion)
{
bucketEntries[i].value = none;
}
// If key was reallocated - it's value becomes essentially
// inaccessible, so we deallocate it.
// All keys, recorded in hash table, guaranteed to be `!= none`.
if (nextEntry.key.GetLifeVersion() != nextEntry.keyLifeVersion)
{
if (bucketEntries[i].value != none && bucketEntries[i].managed) {
bucketEntries[i].value.FreeSelf(nextEntry.keyLifeVersion);
}
bucketEntries.Remove(i, 1);
// We'll update the count, but won't trigger hash table size update
// to avoid making value's indices lookup more expensive, since
// this method is used in `FindEntryIndices()`.
storedElementCount = Max(0, storedElementCount - 1);
continue;
}
i += 1;
}
bucketToClean.entries = bucketEntries;
}
// Checks if we need to change our current capacity and does so if needed
private final function UpdateHashTableCapacity()
{
local int oldCapacity, newCapacity;
oldCapacity = hashTable.length;
// Calculate new capacity (and whether it is needed) based on amount of
// stored properties and current capacity
newCapacity = oldCapacity;
if (storedElementCount < newCapacity * MINIMUM_DENSITY) {
newCapacity /= 2;
}
if (storedElementCount > newCapacity * MAXIMUM_DENSITY) {
newCapacity *= 2;
}
// Enforce our limits
newCapacity = Clamp(newCapacity, MINIMUM_CAPACITY, MAXIMUM_CAPACITY);
// Only resize if difference is huge enough or table does not exists yet
if (newCapacity != oldCapacity) {
ResizeHashTable(newCapacity);
}
}
// Changes size of the hash table, does not check any limits,
// does not check if `newCapacity` is a valid capacity (`newCapacity > 0`).
private final function ResizeHashTable(int newCapacity)
{
local int i, j;
local int newBucketIndex, newEntryIndex;
local array<Entry> bucketEntries;
local array<Bucket> oldHashTable;
oldHashTable = hashTable;
// Clean current hash table
hashTable.length = 0;
hashTable.length = newCapacity;
for (i = 0; i < oldHashTable.length; i += 1)
{
CleanBucket(oldHashTable[i]);
bucketEntries = oldHashTable[i].entries;
for (j = 0; j < bucketEntries.length; j += 1) {
newBucketIndex = GetBucketIndex(bucketEntries[j].key);
newEntryIndex = hashTable[newBucketIndex].entries.length;
hashTable[newBucketIndex].entries[newEntryIndex] = bucketEntries[j];
}
}
}
/**
* Checks if caller `AssociativeArray` has value recorded with a given `key`.
*
* @return `true` if caller `AssociativeArray` has value recorded with
* a given `key` and `false` otherwise.
*/
public final function bool HasKey(AcediaObject key)
{
local int bucketIndex, entryIndex;
return FindEntryIndices(key, bucketIndex, entryIndex);
}
/**
* Checks if caller `AssociativeArray`'s value recorded with a given `key`
* is managed.
*
* Managed values will be automatically deallocated once they are removed
* (or overwritten) from the caller `AssociativeArray`.
*
* @return `true` if value recorded with a given `key` is managed
* and `false` otherwise;
* if value is missing (`none` or there is not entry for the `key`),
* returns `false`.
*/
public final function bool IsManaged(AcediaObject key)
{
local int bucketIndex, entryIndex;
if (FindEntryIndices(key, bucketIndex, entryIndex)) {
return hashTable[bucketIndex].entries[entryIndex].managed;
}
return false;
}
/**
* Returns value recorded by a given key `key` in the caller
* `AssociativeArray`.
*
* Can return `none` if either stored values is `none` or there's no value
* recorded with a `key`. To check whether there is a record, corresponding to
* the `key` use `HasKey()` method.
*
* @param key Key for which to return value.
* @return Value, stored with given key `key`. If there is no value with
* such a key method will return `none`.
*/
public final function AcediaObject GetItem(AcediaObject key)
{
local int bucketIndex, entryIndex;
if (FindEntryIndices(key, bucketIndex, entryIndex)) {
return hashTable[bucketIndex].entries[entryIndex].value;
}
return none;
}
/**
* Returns entry corresponding to a given key `key` in the caller
* `AssociativeArray`, removing it from the caller `AssociativeArray`.
*
* Returned value is no longer managed by the `AssociativeArray` (if it was)
* and must be deallocated once you do not need them anymore.
*
* @param key Key for which to return entry.
* @return Entry (key/value pair + indicator of whether values was managed
* by `AssociativeArray`) with the given key `key`.
*/
public final function Entry TakeEntry(AcediaObject key)
{
local Entry entryToTake;
local int bucketIndex, entryIndex;
if (!FindEntryIndices(key, bucketIndex, entryIndex)) {
return entryToTake;
}
entryToTake = hashTable[bucketIndex].entries[entryIndex];
hashTable[bucketIndex].entries.Remove(entryIndex, 1);
storedElementCount = Max(0, storedElementCount - 1);
UpdateHashTableCapacity();
return entryToTake;
}
/**
* Returns value recorded with a given key `key` in the caller
* `AssociativeArray`, removing it from the collection.
*
* Returned value is no longer managed by the `AssociativeArray` (if it was)
* and must be deallocated once you do not need it anymore.
*
* @param key Key for which to return value.
* @return Value, stored with given key `key`. If there is no value with
* such a key method will return `none`.
*/
public final function AcediaObject TakeItem(AcediaObject key)
{
return TakeEntry(key).value;
}
/**
* Records new `value` under the key `key` into the caller `AssociativeArray`.
*
* If this will override already existing managed record - old value will
* be automatically deallocated (unless they are the same object as a new one).
* If you wish to avoid this behavior - retrieve them with either of
* `TakeItem()` or `TakeEntry()` methods first.
*
* @param key Key by which new value will be referred to.
* @param value Value to store in the caller `AssociativeArray`.
* @return Caller `AssociativeArray` to allow for method chaining.
*/
public final function AssociativeArray SetItem(
AcediaObject key,
AcediaObject value,
optional bool managed)
{
local Entry oldEntry, newEntry;
local int bucketIndex, entryIndex;
if (key == none) {
return self;
}
if (FindEntryIndices(key, bucketIndex, entryIndex)) {
oldEntry = hashTable[bucketIndex].entries[entryIndex];
}
else {
storedElementCount += 1;
}
newEntry.key = key;
newEntry.keyLifeVersion = key.GetLifeVersion();
newEntry.managed = managed;
newEntry.value = value;
if (value != none) {
newEntry.valueLifeVersion = value.GetLifeVersion();
}
if ( oldEntry.managed && oldEntry.value != none
&& newEntry.value != oldEntry.value)
{
oldEntry.value.FreeSelf(oldEntry.valueLifeVersion);
}
hashTable[bucketIndex].entries[entryIndex] = newEntry;
return self;
}
/**
* Creates a new instance of class `valueClass` and records it's value with
* key `key` in the caller `AssociativeArray`. Value is recorded as managed.
*
* @param key Key by which new value will be referred to.
* @param valueClass Class of object to create. Will only be created if
* passed `key` is valid.
* @return Caller `AssociativeArray` to allow for method chaining.
*/
public final function AssociativeArray CreateItem(
AcediaObject key,
class<AcediaObject> valueClass)
{
if (key == none) return self;
if (valueClass == none) return self;
return SetItem(key, AcediaObject(_.memory.Allocate(valueClass)), true);
}
/**
* Removes a value recorded with a given key `key`.
* Does nothing if entry with a given key does not exist.
*
* Removed values are deallocated if they are managed. If you wish to avoid
* that, use `TakeItem()` or `TakeEntry()` methods.
*
* @param key Key for which to remove value.
* @return Caller `AssociativeArray` to allow for method chaining.
*/
public final function AssociativeArray RemoveItem(AcediaObject key)
{
local Entry entryToRemove;
local int bucketIndex, entryIndex;
if (key == none) return self;
if (!FindEntryIndices(key, bucketIndex, entryIndex)) {
return self;
}
entryToRemove = hashTable[bucketIndex].entries[entryIndex];
hashTable[bucketIndex].entries.Remove(entryIndex, 1);
storedElementCount = Max(0, storedElementCount - 1);
UpdateHashTableCapacity();
if (entryToRemove.managed && entryToRemove.value != none) {
entryToRemove.value.FreeSelf(entryToRemove.valueLifeVersion);
}
return self;
}
/**
* Completely clears caller `AssociativeArray` of all stored entries,
* deallocating any stored managed values.
*
* @return Caller `AssociativeArray` to allow for method chaining.
*/
public function Empty()
{
local int i, j;
local array<Entry> nextEntries;
for (i = 0; i < hashTable.length; i += 1)
{
nextEntries = hashTable[i].entries;
for (j = 0; j < nextEntries.length; j += 1)
{
if (!nextEntries[j].managed) continue;
if (nextEntries[j].value == none) continue;
nextEntries[j].value.FreeSelf(nextEntries[j].valueLifeVersion);
}
}
hashTable.length = 0;
storedElementCount = 0;
}
/**
* Returns key of all properties inside caller `AssociativeArray`.
*
* Collecting all keys from the `AssociativeArray` is O(<number_of_elements>).
*
* @return Array of all the caller `AssociativeArray`'s keys.
*/
public final function array<AcediaObject> GetKeys()
{
local int i, j;
local array<AcediaObject> result;
local array<Entry> nextEntry;
for (i = 0; i < hashTable.length; i += 1)
{
//hashTable[i] = CleanBucket(hashTable[i]);
CleanBucket(hashTable[i]);
nextEntry = hashTable[i].entries;
for (j = 0; j < nextEntry.length; j += 1) {
result[result.length] = nextEntry[j].key;
}
}
return result;
}
/**
* Returns amount of elements in the caller `AssociativeArray`.
*
* Note that this value might overestimate real amount of values inside
* `AssociativeArray` in case some of the keys used for storage were
* deallocated by code outside of `AssociativeArray`.
* Such values might be eventually found and removed, but
* `AssociativeArray` does not provide any guarantees on when it's done.
*/
public final function int GetLength()
{
return storedElementCount;
}
/**
* Auxiliary method for iterator that increments given `Index` structure.
*
* @param previousIndex Index to increment.
* @return `true` if incremented index is pointing at a valid item,
* `false` if collection has ended.
*/
public final function bool IncrementIndex(out Index previousIndex)
{
previousIndex.entryIndex += 1;
// Go forward through buckets until we find non-empty one
while (previousIndex.bucketIndex < hashTable.length)
{
CleanBucket(hashTable[previousIndex.bucketIndex]);
if ( previousIndex.entryIndex
< hashTable[previousIndex.bucketIndex].entries.length)
{
return true;
}
previousIndex.entryIndex = 0;
previousIndex.bucketIndex += 1;
}
return false;
}
/**
* Auxiliary method for iterator that returns value corresponding to
* a given `Index` structure.
*
* @param index Index of item to return.
* @return `Entry` corresponding to a given index. If index is invalid
* (not pointing at any value for caller `AssociativeArray`) returns
* `Entry` with key and value set to `none`.
* Note that `none` can be returned because that is simply the value
* being stored.
*/
public final function Entry GetEntryByIndex(Index index)
{
local Entry emptyEntry;
if (index.bucketIndex < 0) return emptyEntry;
if (index.bucketIndex >= hashTable.length) return emptyEntry;
if ( index.entryIndex < 0
|| index.entryIndex >= hashTable[index.bucketIndex].entries.length) {
return emptyEntry;
}
return hashTable[index.bucketIndex].entries[index.entryIndex];
}
defaultproperties
{
iteratorClass = class'AssociativeArrayIterator'
MINIMUM_CAPACITY = 50
MAXIMUM_CAPACITY = 10000
MINIMUM_DENSITY = 0.25
MAXIMUM_DENSITY = 0.75
}

83
sources/Data/Collections/AssociativeArrayIterator.uc

@ -0,0 +1,83 @@
/**
* Iterator for iterating over `AssociativeArray`'s items.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AssociativeArrayIterator extends Iter
dependson(AssociativeArray);
var private bool hasNotFinished;
var private AssociativeArray relevantCollection;
var private AssociativeArray.Index currentIndex;
protected function Finalizer()
{
relevantCollection = none;
}
public function bool Initialize(Collection relevantArray)
{
local AssociativeArray.Index emptyIndex;
currentIndex = emptyIndex;
relevantCollection = AssociativeArray(relevantArray);
if (relevantCollection == none) {
return false;
}
hasNotFinished = (relevantCollection.GetLength() > 0);
if (GetKey() == none) {
relevantCollection.IncrementIndex(currentIndex);
}
return true;
}
public function Iter Next(optional bool skipNone)
{
local int collectionLength;
if (!skipNone)
{
hasNotFinished = relevantCollection.IncrementIndex(currentIndex);
return self;
}
collectionLength = relevantCollection.GetLength();
while (hasNotFinished)
{
hasNotFinished = relevantCollection.IncrementIndex(currentIndex);
if (relevantCollection.GetEntryByIndex(currentIndex).value != none) {
return self;
}
}
return self;
}
public function AcediaObject Get()
{
return relevantCollection.GetEntryByIndex(currentIndex).value;
}
public function AcediaObject GetKey()
{
return relevantCollection.GetEntryByIndex(currentIndex).key;
}
public function bool HasFinished()
{
return !hasNotFinished;
}
defaultproperties
{
}

51
sources/Data/Collections/Collection.uc

@ -0,0 +1,51 @@
/**
* Acedia provides a small set of collections for easier data storage.
* This is their base class that provides a simple interface for
* common methods.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Collection extends AcediaObject
abstract;
var class<Iter> iteratorClass;
/**
* Creates an `Iterator` instance to iterate over stored items.
*
* Returned `Iterator` must be manually deallocated after it was used.
*
* @return New initialized `Iterator` that will iterate over all items in
* a given collection. Guaranteed to be not `none`.
*/
public final function Iter Iterate()
{
local Iter newIterator;
newIterator = Iter(_.memory.Allocate(iteratorClass));
if (!newIterator.Initialize(self))
{
// This should not ever happen.
// If it does - it is a bug.
newIterator.FreeSelf();
return none;
}
return newIterator;
}
defaultproperties
{
}

98
sources/Data/Collections/CollectionsAPI.uc

@ -0,0 +1,98 @@
/**
* Convenience API that provides methods for quickly creating collections.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CollectionsAPI extends AcediaObject;
/**
* Creates a new `DynamicArray`, optionally filling it with objects from
* a given native array.
*
* @param objectArray Objects to place inside created `DynamicArray`;
* if empty (by default) - new, empty `DynamicArray` will be returned.
* Objects will be added in the same order as in `objectArray`.
* @param managed Flag that indicates whether objects from
* `objectArray` argument should be added as managed.
* By default `false` - they would not be managed.
* @return New `DynamicArray`, optionally filled with contents of
* `objectArray`. Guaranteed to be not `none` and to not contain any items
* outside of `objectArray`.
*/
public final function DynamicArray NewDynamicArray(
array<AcediaObject> objectArray,
optional bool managed)
{
local int i;
local DynamicArray result;
result = DynamicArray(_.memory.Allocate(class'DynamicArray'));
for (i = 0; i < objectArray.length; i += 1) {
result.AddItem(objectArray[i], managed);
}
return result;
}
/**
* Creates a new empty `DynamicArray`.
*
* @return New empty instance of `DynamicArray`.
*/
public final function DynamicArray EmptyDynamicArray()
{
return DynamicArray(_.memory.Allocate(class'DynamicArray'));
}
/**
* Creates a new `AssociativeArray`, optionally filling it with entries
* (key/value pairs) from a given native array.
*
* @param entriesArray Entries (key/value pairs) to place inside created
* `AssociativeArray`; if empty (by default) - new,
* empty `AssociativeArray` will be returned.
* @param managed Flag that indicates whether values from
* `entriesArray` argument should be added as managed.
* By default `false` - they would not be managed.
* @return New `AssociativeArray`, optionally filled with contents of
* `entriesArray`. Guaranteed to be not `none` and to not contain any items
* outside of `entriesArray`.
*/
public final function AssociativeArray NewAssociativeArray(
array<AssociativeArray.Entry> entriesArray,
optional bool managed)
{
local int i;
local AssociativeArray result;
result = AssociativeArray(_.memory.Allocate(class'AssociativeArray'));
for (i = 0; i < entriesArray.length; i += 1) {
result.SetItem(entriesArray[i].key, entriesArray[i].value, managed);
}
return result;
}
/**
* Creates a new empty `AssociativeArray`.
*
* @return New empty instance of `AssociativeArray`.
*/
public final function AssociativeArray EmptyAssociativeArray()
{
return AssociativeArray(_.memory.Allocate(class'AssociativeArray'));
}
defaultproperties
{
}

480
sources/Data/Collections/DynamicArray.uc

@ -0,0 +1,480 @@
/**
* Dynamic array object for storing arbitrary types of data. Generic
* storage is achieved by using `AcediaObject` as the stored type. Native
* variable types such as `int`, `bool`, etc. can be stored by boxing them into
* `AcediaObject`s.
* Appropriate classes and APIs for their construction are provided for
* main primitive types and can be extended to any custom `struct`.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DynamicArray extends Collection;
// Actual storage of all our data.
var private array<AcediaObject> storedObjects;
// `managedFlags[i] > 0` iff `contents[i]` is a managed object.
// Invariant `managedFlags.length == contents.length` should be enforced by
// all methods.
var private array<byte> managedFlags;
// Recorded `lifeVersions` of all stored objects.
// Invariant `lifeVersions.length == contents.length` should be enforced by
// all methods.
var private array<int> lifeVersions;
// Free array data
protected function Finalizer()
{
Empty();
}
// Method, used to compare array values at different indices.
// Does not check boundary conditions, so make sure passed indices are valid.
private function bool AreEqual(AcediaObject object1, AcediaObject object2)
{
if (object1 == none && object2 == none) return true;
if (object1 == none || object2 == none) return false;
return object1.IsEqual(object2);
}
/**
* Returns current length of dynamic `DynamicArray`.
* Cannot fail.
*
* @return Returns length of the caller `DynamicArray`.
* Guaranteed to be non-negative.
*/
public final function int GetLength()
{
return storedObjects.length;
}
/**
* Changes length of the caller `DynamicArray`.
* If `DynamicArray` size is increased as a result - added items will be
* filled with `none`s.
* If `DynamicArray` size is decreased - erased managed items will be
* automatically deallocated.
*
* @param newLength New length of an `DynamicArray`.
* If negative value is passes - method will do nothing.
* @return Reference to the caller `DynamicArray` to allow for method chaining.
*/
public final function DynamicArray SetLength(int newLength)
{
local int i;
if (newLength < 0) {
return self;
}
for (i = newLength; i < storedObjects.length; i += 1) {
FreeManagedItem(i);
}
storedObjects.length = newLength;
managedFlags.length = newLength;
lifeVersions.length = newLength;
return self;
}
/**
* Deallocates an item at a given index `index`, if it's managed.
* Does not check `DynamicArray` bounds for `index`, so you must ensure that
* `index` is valid.
*
* @param index Index of the managed item to deallocate.
* @return Reference to the caller `DynamicArray` to allow for method chaining.
*/
protected final function DynamicArray FreeManagedItem(int index)
{
if (storedObjects[index] == none) return self;
if (!storedObjects[index].IsAllocated()) return self;
if (managedFlags[index] <= 0) return self;
if (lifeVersions[index] != storedObjects[index].GetLifeVersion()) {
return self;
}
if ( storedObjects[index] != none && managedFlags[index] > 0
&& lifeVersions[index] == storedObjects[index].GetLifeVersion())
{
storedObjects[index].FreeSelf();
storedObjects[index] = none;
}
return self;
}
/**
* Empties caller `DynamicArray`, erasing it's contents.
* All managed objects will be deallocated.
*
* @return Reference to the caller `DynamicArray` to allow for method chaining.
*/
public final function DynamicArray Empty()
{
SetLength(0);
return self;
}
/**
* Adds `amountOfNewItems` empty (`none`) items at the end of
* the `DynamicArray`.
* To insert items at an arbitrary array index, use `Insert()`.
*
* @param amountOfNewItems Amount of items to add at the end.
* If non-positive value is passed, - method does nothing.
* @return Reference to the caller `DynamicArray` to allow for method chaining.
*/
public final function DynamicArray Add(int amountOfNewItems)
{
if (amountOfNewItems > 0) {
SetLength(storedObjects.length + amountOfNewItems);
}
return self;
}
/**
* Inserts `count` empty (`none`) items into the `DynamicArray`
* at specified position.
* The indices of the following items are increased by `count` in order
* to make room for the new items.
*
* To add items at the end of an `DynamicArray`, consider using `Add()`,
* which is equivalent to `array.Insert(array.GetLength(), ...)`.
*
* @param index Index, where first inserted item will be located.
* Must belong to `[0; self.GetLength()]` inclusive interval,
* otherwise method does nothing.
* @param count Amount of new items to insert.
* Must be positive, otherwise method does nothing.
* @return Reference to the caller `DynamicArray` to allow for method chaining.
*/
public final function DynamicArray Insert(int index, int count)
{
local int i;
local int swapIndex;
local int amountToShift;
if (count <= 0) return self;
if (index < 0 || index > storedObjects.length) return self;
amountToShift = storedObjects.length - index;
Add(count);
if (amountToShift == 0) {
return self;
}
for (i = 0; i < amountToShift; i += 1)
{
swapIndex = storedObjects.length - i - 1;
Swap(swapIndex, swapIndex - count);
}
return self;
}
/**
* Swaps two `DynamicArray` items, along with information about their
* managed status.
*
* @param index1 Index of item to swap.
* @param index2 Index of item to swap.
*/
protected final function Swap(int index1, int index2)
{
local AcediaObject temporaryItem;
local int temporaryNumber;
// Swap object
temporaryItem = storedObjects[index1];
storedObjects[index1] = storedObjects[index2];
storedObjects[index2] = temporaryItem;
// Swap life versions
temporaryNumber = lifeVersions[index1];
lifeVersions[index1] = lifeVersions[index2];
lifeVersions[index2] = temporaryNumber;
// Swap managed flags
temporaryNumber = managedFlags[index1];
managedFlags[index1] = managedFlags[index2];
managedFlags[index2] = temporaryNumber;
}
/**
* Removes number items from the `DynamicArray`, starting at `index`.
* All items before position and from `index + count` on are not changed,
* but the item indices change, - they shift to close the gap,
* created by removed items.
*
* @param index Remove items starting from this index.
* Must belong to `[0; self.GetLength() - 1]` inclusive interval,
* otherwise method does nothing.
* @param count Removes at most this much items.
* Must be positive, otherwise method does nothing.
* Specifying more items than can be removed simply removes
* all items, starting from `index`.
* @return Reference to the caller `DynamicArray` to allow for method chaining.
*/
public final function DynamicArray Remove(int index, int count)
{
local int i;
if (count <= 0) return self;
if (index < 0 || index > storedObjects.length) return self;
count = Min(count, storedObjects.length - index);
for (i = 0; i < count; i += 1) {
FreeManagedItem(index + i);
}
storedObjects.Remove(index, count);
managedFlags.Remove(index, count);
lifeVersions.Remove(index, count);
return self;
}
/**
* Removes item at a given index, shifting all the items that come after
* one place backwards.
*
* @param index Remove items starting from this index.
* Must belong to `[0; self.GetLength() - 1]` inclusive interval,
* otherwise method does nothing.
* @return Reference to the caller `DynamicArray` to allow for method chaining.
*/
public final function DynamicArray RemoveIndex(int index)
{
Remove(index, 1);
return self;
}
/**
* Checks if caller `DynamicArray`'s value at index `index` is managed.
*
* Managed values will be automatically deallocated once they are removed
* (or overwritten) from the caller `DynamicArray`.
*
* @return `true` if value, recorded in caller `DynamicArray` at index `index`
* is managed and `false` otherwise.
* If `index` is invalid (outside of `DynamicArray` bounds)
* also returns `false`.
*/
public final function bool IsManaged(int index)
{
if (index < 0) return false;
if (index >= storedObjects.length) return false;
if (storedObjects[index] == none) return false;
if (!storedObjects[index].IsAllocated()) return false;
if (storedObjects[index].GetLifeVersion() != lifeVersions[index]) {
return false;
}
return (managedFlags[index] > 0);
}
/**
* Returns item at `index` and replaces it with `none` inside `DynamicArray`.
* If index is invalid, returns `none`.
*
* If returned value was managed, it won't be deallocated
* and will stop being managed.
*
* @param index Index of an item that `DynamicArray` has to return.
* @return Either value at `index` in the caller `DynamicArray` or `none` if
* passed `index` is invalid.
*/
public final function AcediaObject TakeItem(int index)
{
local AcediaObject result;
if (index < 0) return none;
if (index >= storedObjects.length) return none;
if (storedObjects[index] == none) return none;
if (!storedObjects[index].IsAllocated()) return none;
if (storedObjects[index].GetLifeVersion() != lifeVersions[index]) {
return none;
}
result = storedObjects[index];
storedObjects[index] = none;
managedFlags[index] = 0;
lifeVersions[index] = 0;
return result;
}
/**
* Returns item at `index`. If index is invalid, returns `none`.
*
* @param index Index of an item that `DynamicArray` has to return.
* @return Either value at `index` in the caller `DynamicArray` or `none` if
* passed `index` is invalid.
*/
public final function AcediaObject GetItem(int index)
{
if (index < 0) return none;
if (index >= storedObjects.length) return none;
if (storedObjects[index] == none) return none;
if (!storedObjects[index].IsAllocated()) return none;
if (storedObjects[index].GetLifeVersion() != lifeVersions[index]) {
return none;
}
return storedObjects[index];
}
/**
* Changes `DynamicArray`'s value at `index` to `item`.
*
* @param index Index, at which to change the value. If `DynamicArray` is
* not long enough to hold it, it will be automatically expanded.
* If passed index is negative - method will do nothing.
* @param item Value to be set at a given index.
* @param managed Whether `item` should be managed by `DynamicArray`.
* By default (`false`) all items are not managed.
* @return Reference to the caller `DynamicArray` to allow for method chaining.
*/
public final function DynamicArray SetItem(
int index,
AcediaObject item,
optional bool managed)
{
if (index < 0) {
return self;
}
if (index >= storedObjects.length) {
SetLength(index + 1);
}
else if (item != storedObjects[index]) {
FreeManagedItem(index);
}
storedObjects[index] = item;
managedFlags[index] = 0;
if (managed) {
managedFlags[index] = 1;
}
if (item != none) {
lifeVersions[index] = item.GetLifeVersion();
}
return self;
}
/**
* Creates a new instance of class `valueClass` and records it's value at index
* `index` in the caller `DynamicArray`. Value is recorded as managed.
*
* @param index Index, at which to change the value. If `DynamicArray`
* is not long enough to hold it, it will be automatically expanded.
* If passed index is negative - method will do nothing.
* @param valueClass Class of object to create. Will only be created if
* passed `index` is valid.
* @return Caller `DynamicArray` to allow for method chaining.
*/
public final function DynamicArray CreateItem(
int index,
class<AcediaObject> valueClass)
{
if (index < 0) return self;
if (valueClass == none) return self;
return SetItem(index, AcediaObject(_.memory.Allocate(valueClass)), true);
}
/**
* Adds given `item` at the end of the `DynamicArray`, expanding it by
* one item.
* Cannot fail.
*
* @param item Item to be added at the end of the `DynamicArray`.
* @param managed Whether `item` should be managed by `DynamicArray`.
* By default (`false`) all items are not managed.
* @return Reference to the caller `DynamicArray` to allow for method chaining.
*/
public final function DynamicArray AddItem(
AcediaObject item,
optional bool managed)
{
return SetItem(storedObjects.length, item, managed);
}
/**
* Inserts given `item` at index `index` of the `DynamicArray`,
* shifting all the items starting from `index` one position to the right.
* Cannot fail.
*
* @param index Index at which to insert new item. Must belong to
* inclusive range `[0; self.GetLength()]`, otherwise method does nothing.
* @param item Item to insert.
* @param managed Whether `item` should be managed by `DynamicArray`.
* By default (`false`) all items are not managed.
* @return Reference to the caller `DynamicArray` to allow for method chaining.
*/
public final function DynamicArray InsertItem(
int index,
AcediaObject item,
optional bool managed)
{
if (index < 0) return self;
if (index > storedObjects.length) return self;
Insert(index, 1);
SetItem(index, item, managed);
return self;
}
/**
* Returns all occurrences of `item` in the caller `DynamicArray`
* (optionally only first one).
*
* @param item Item that needs to be removed from a `DynamicArray`.
* @param onlyFirstItem Set to `true` to only remove first occurrence.
* By default `false`, which means all occurrences will be removed.
* @return Reference to the caller `DynamicArray` to allow for method chaining.
*/
public final function DynamicArray RemoveItem(
AcediaObject item,
optional bool onlyFirstItem)
{
local int i;
while (i < storedObjects.length)
{
if (AreEqual(storedObjects[i], item))
{
Remove(i, 1);
if (onlyFirstItem) {
return self;
}
}
else {
i += 1;
}
}
return self;
}
/**
* Finds first occurrence of `item` in caller `DynamicArray` and returns
* it's index.
*
* @param item Item to find in `DynamicArray`.
* @return Index of first occurrence of `item` in caller `DynamicArray`.
* `-1` if `item` is not found.
*/
public final function int Find(AcediaObject item)
{
local int i;
for (i = 0; i < storedObjects.length; i += 1)
{
if (AreEqual(storedObjects[i], item)) {
return i;
}
}
return -1;
}
defaultproperties
{
iteratorClass = class'DynamicArrayIterator'
}

80
sources/Data/Collections/DynamicArrayIterator.uc

@ -0,0 +1,80 @@
/**
* Iterator for iterating over `DynamicArray`'s items.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DynamicArrayIterator extends Iter;
var private DynamicArray relevantCollection;
var private int currentIndex;
protected function Finalizer()
{
relevantCollection = none;
}
public function bool Initialize(Collection relevantArray)
{
currentIndex = 0;
relevantCollection = DynamicArray(relevantArray);
if (relevantCollection == none) {
return false;
}
return true;
}
public function Iter Next(optional bool skipNone)
{
local int collectionLength;
if (!skipNone)
{
currentIndex += 1;
return self;
}
collectionLength = relevantCollection.GetLength();
while (currentIndex < collectionLength)
{
currentIndex += 1;
if (relevantCollection.GetItem(currentIndex) != none) {
return self;
}
}
return self;
}
public function AcediaObject Get()
{
return relevantCollection.GetItem(currentIndex);
}
/**
* Note that for `DynamicArrayIterator` this method produces a new `IntBox`
* object each time and requires manual deallocation.
*/
public function AcediaObject GetKey()
{
return _.box.int(currentIndex);
}
public function bool HasFinished()
{
return currentIndex >= relevantCollection.GetLength();
}
defaultproperties
{
}

91
sources/Data/Collections/Iter.uc

@ -0,0 +1,91 @@
/**
* Base class for iterator, an auxiliary object for iterating through
* objects stored inside an Acedia's collection.
* Iterators expect that collection remains unchanged while they
* are iterating through it. Otherwise their behavior becomes undefined.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Iter extends AcediaObject
abstract;
/**
* Initialized caller `Iterator` to iterate over a given collection.
*
* `Iterator` should only be initialized once, reinitializing `Iterator` is
* considered an undefined behavior. Collection's `Iterate()` method is
* a preferred method to create initialized `Iterator`.
*
* Initialization is not guaranteed to be successful, - each iterator class
* corresponds to a particular collection and it's not `none` reference must
* be used as an argument.
*
* @param relevantCollection `Collection` over which items `Iterator`
* must iterate.
* @param `true` if iteration was successful and `false` otherwise.
*/
public function bool Initialize(Collection relevantCollection);
/**
* Makes iterator pick next item in collection.
* As long as collection that's being iterated over is not modified,
* `Next()` is guaranteed to iterate over all it's items.
* Use `HasFinished()` to check whether you have iterated all of them.
* Order of iteration is not guaranteed.
*
* @param skipNone Set this to `true` if you want to skip all stored
* values that are equal to `none`. By default does not skip them.
* Since order of iterating through items is not guaranteed, at each
* `Next()` call an arbitrary set of items can be skipped.
* @return Reference to caller `Iterator` to allow for method chaining.
*/
public function Iter Next(optional bool skipNone);
/**
* Returns current value pointed to by an iterator.
*
* Does not advance iteration: use `Next()` to pick next value.
*
* @return Current value being iterated over. If `Iterator()` has finished
* iterating over all values or was not initialized - returns `none`.
* Note that `none` can also be returned if it's stored in a collection.
*/
public function AcediaObject Get();
/**
* Returns key of current value pointed to by an iterator.
*
* Does not advance iteration: use `Next()` to pick next value.
*
* @return Key of the current value being iterated over.
* If `Iterator()` has finished iterating over all values or
* was not initialized - returns `none`.
* Note that `none` can also be returned if it's stored in a collection.
*/
public function AcediaObject GetKey();
/**
* Checks if caller `Iterator` has finished iterating.
*
* @return `true` if caller `Iterator` has finished iterating or
* was not initialized. `false` otherwise.
*/
public function bool HasFinished();
defaultproperties
{
}

48
sources/Data/Collections/Tests/MockItem.uc

@ -0,0 +1,48 @@
/**
* Mock object class for testing how collections deal with managed objects.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MockItem extends AcediaObject;
var public int objectCount;
protected function Constructor()
{
default.objectCount += 1;
}
protected function Finalizer()
{
default.objectCount -= 1;
}
// We don't want to differentiate between these objects
public function bool IsEqual(Object other)
{
return true;
}
public function int GetHashCode()
{
return 0;
}
defaultproperties
{
objectCount = 0
}

356
sources/Data/Collections/Tests/TEST_AssociativeArray.uc

@ -0,0 +1,356 @@
/**
* Set of tests for `AssociativeArray` class.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_AssociativeArray extends TestCase
abstract;
protected static function TESTS()
{
Test_GetSet();
Test_HasKey();
Test_GetKeys();
Test_Remove();
Test_CreateItem();
Test_Empty();
Test_Length();
Test_Managed();
Test_DeallocationHandling();
Test_Take();
Test_LargeArray();
}
protected static function Test_GetSet()
{
local Text textObject;
local AssociativeArray array;
array = AssociativeArray(__().memory.Allocate(class'AssociativeArray'));
Context("Testing getters and setters for items of `AssociativeArray`.");
Issue("`SetItem()` does not correctly set new items.");
textObject = __().text.FromString("value");
array.SetItem(__().text.FromString("key"), textObject);
array.SetItem(__().box.int(13), __().text.FromString("value #2"));
array.SetItem(__().box.float(345.2), __().box.bool(true));
TEST_ExpectTrue( Text(array.GetItem(__().box.int(13))).ToPlainString()
== "value #2");
TEST_ExpectTrue(array.GetItem(__().text.FromString("key")) == textObject);
TEST_ExpectTrue(BoolBox(array.GetItem(__().box.float(345.2))).Get());
Issue("`SetItem()` does not correctly overwrite new items.");
array.SetItem(__().text.FromString("key"), __().text.FromString("value"));
array.SetItem(__().box.int(13), __().box.int(11));
TEST_ExpectFalse(array.GetItem(__().text.FromString("key")) == textObject);
TEST_ExpectTrue( Text(array.GetItem(__().text.FromString("key")))
.ToPlainString() == "value");
TEST_ExpectTrue( IntBox(array.GetItem(__().box.int(13))).Get()
== 11);
Issue("`GetItem()` does not return `none` for non-existing keys.");
TEST_ExpectNone(array.GetItem(__().box.int(12)));
TEST_ExpectNone(array.GetItem(__().box.byte(67)));
TEST_ExpectNone(array.GetItem(__().box.float(43.1234)));
TEST_ExpectNone(array.GetItem(__().text.FromString("Some random stuff")));
}
protected static function Test_HasKey()
{
local AssociativeArray array;
array = AssociativeArray(__().memory.Allocate(class'AssociativeArray'));
Context("Testing `HasKey()` method for `AssociativeArray`.");
array.SetItem(__().text.FromString("key"), __().text.FromString("value"));
array.SetItem(__().box.int(13), __().text.FromString("value #2"));
array.SetItem(__().box.float(345.2), __().box.bool(true));
Issue("`HasKey()` reports that added keys do not exist in"
@ "`AssociativeArray`.");
TEST_ExpectTrue(array.HasKey(__().text.FromString("key")));
TEST_ExpectTrue(array.HasKey(__().box.int(13)));
TEST_ExpectTrue(array.HasKey(__().box.float(345.2)));
Issue("`HasKey()` reports that `AssociativeArray` contains keys that"
@ "were never added.");
TEST_ExpectFalse(array.HasKey(none));
TEST_ExpectFalse(array.HasKey(__().box.float(13)));
TEST_ExpectFalse(array.HasKey(__().box.byte(139)));
}
protected static function Test_GetKeys()
{
local int i;
local AcediaObject key1, key2, key3;
local array<AcediaObject> keys;
local AssociativeArray array;
array = AssociativeArray(__().memory.Allocate(class'AssociativeArray'));
Context("Testing `GetKeys()` method for `AssociativeArray`.");
key1 = __().text.FromString("key");
key2 = __().box.int(13);
key3 = __().box.float(345.2);
array.SetItem(key1, __().text.FromString("value"));
array.SetItem(key2, __().text.FromString("value #2"));
array.SetItem(key3, __().box.bool(true));
keys = array.GetKeys();
Issue("`GetKeys()` returns array with wrong amount of elements.");
TEST_ExpectTrue(keys.length == 3);
Issue("`GetKeys()` returns array with duplicate keys.");
TEST_ExpectTrue(keys[0] != keys[1]);
TEST_ExpectTrue(keys[0] != keys[2]);
TEST_ExpectTrue(keys[1] != keys[2]);
Issue("`GetKeys()` returns array with incorrect keys.");
for (i = 0; i < 3; i += 1) {
TEST_ExpectTrue(keys[i] == key1 || keys[i] == key2 || keys[i] == key3);
}
keys = array.RemoveItem(key1).GetKeys();
Issue("`GetKeys()` returns array with incorrect keys after removing"
@ "an element.");
TEST_ExpectTrue(keys.length == 2);
TEST_ExpectTrue(keys[0] != keys[1]);
for (i = 0; i < 2; i += 1) {
TEST_ExpectTrue(keys[i] == key2 || keys[i] == key3);
}
}
protected static function Test_Remove()
{
local AcediaObject key1, key2, key3;
local array<AcediaObject> keys;
local AssociativeArray array;
array = AssociativeArray(__().memory.Allocate(class'AssociativeArray'));
Context("Testing removing elements from `AssociativeArray`.");
key1 = __().text.FromString("some key");
key2 = __().box.int(25);
key3 = __().box.float(0.07);
array.SetItem(key1, __().text.FromString("value"));
array.SetItem(key2, __().text.FromString("value #2"));
array.SetItem(key3, __().box.bool(true));
Issue("Elements are not properly removed from `AssociativeArray`.");
array.RemoveItem(key1)
.RemoveItem(__().box.int(25))
.RemoveItem(__().box.float(0.06));
keys = array.GetKeys();
TEST_ExpectTrue(array.GetLength() == 1);
TEST_ExpectTrue(keys.length == 1);
TEST_ExpectTrue(keys[0] == key3);
}
protected static function Test_CreateItem()
{
local AssociativeArray array;
array = AssociativeArray(__().memory.Allocate(class'AssociativeArray'));
Context("Testing creating brand new items for `AssociativeArray`.");
Issue("`CreateItem()` incorrectly adds new values to the"
@ "`AssociativeArray`.");
array.CreateItem(__().text.FromString("key"), class'Text');
array.CreateItem(__().box.float(17.895), class'IntRef');
array.CreateItem(__().text.FromString("key #2"), class'BoolBox');
TEST_ExpectTrue(Text(array.GetItem(__().text.FromString("key")))
.ToPlainString() == "");
TEST_ExpectTrue( IntRef(array.GetItem(__().box.float(17.895))).Get()
== 0);
TEST_ExpectFalse(BoolBox(array.GetItem(__().text.FromString("key #2")))
.Get());
Issue("`CreateItem()` incorrectly overrides existing values in the"
@ "`AssociativeArray`.");
array.SetItem(__().box.int(13), __().ref.int(7));
array.CreateItem(__().box.int(13), class'StringRef');
TEST_ExpectTrue( StringRef(array.GetItem(__().box.int(13))).Get()
== "");
class'MockItem'.default.objectCount = 0;
Issue("`CreateItem()` creates new object even if it cannot be recorded with"
@ "a given key.");
array.CreateItem(none, class'MockItem');
TEST_ExpectTrue(class'MockItem'.default.objectCount == 0);
}
protected static function Test_Empty()
{
local AssociativeArray array;
array = AssociativeArray(__().memory.Allocate(class'AssociativeArray'));
Context("Testing `Empty()` method for `AssociativeArray`.");
array.SetItem(__().text.FromString("key"), __().text.FromString("value"));
array.SetItem(__().box.int(13), __().text.FromString("value #2"));
array.SetItem(__().box.float(345.2), __().box.bool(true));
Issue("`AssociativeArray` still contains elements after being emptied.");
array.Empty();
TEST_ExpectTrue(array.GetKeys().length == 0);
TEST_ExpectTrue(array.GetLength() == 0);
}
protected static function Test_Length()
{
local AssociativeArray array;
array = AssociativeArray(__().memory.Allocate(class'AssociativeArray'));
Context("Testing computing length of `AssociativeArray`.");
Issue("Length is not zero for newly created `AssociativeArray`.");
TEST_ExpectTrue(array.GetLength() == 0);
Issue("Length is incorrectly computed after adding elements to"
@ "`AssociativeArray`.");
array.SetItem(__().text.FromString("key"), __().text.FromString("value"));
array.SetItem(__().box.int(4563), __().text.FromString("value #2"));
array.SetItem(__().box.float(3425.243), __().box.byte(23));
TEST_ExpectTrue(array.GetLength() == 3);
Issue("Length is incorrectly computed after removing elements from"
@ "`AssociativeArray`.");
array.RemoveItem(__().box.int(4563));
TEST_ExpectTrue(array.GetLength() == 2);
}
protected static function MockItem NewMockItem()
{
return MockItem(__().memory.Allocate(class'MockItem'));
}
protected static function Test_Managed()
{
local AssociativeArray array;
class'MockItem'.default.objectCount = 0;
array = AssociativeArray(__().memory.Allocate(class'AssociativeArray'));
array.SetItem(__().box.int(0), NewMockItem());
array.SetItem(__().box.int(1), NewMockItem());
array.SetItem(__().box.int(2), NewMockItem());
array.SetItem(__().box.int(3), NewMockItem(), true);
array.SetItem(__().box.int(4), NewMockItem(), true);
array.SetItem(__().box.int(5), NewMockItem(), true);
array.CreateItem(__().box.int(6), class'MockItem');
array.CreateItem(__().box.int(7), class'MockItem');
array.CreateItem(__().box.int(8), class'MockItem');
Context("Testing how `AssociativeArray` deallocates managed objects.");
Issue("`RemoveItem()` incorrectly deallocates managed items.");
array.RemoveItem(__().box.int(0));
array.RemoveItem(__().box.int(3));
array.RemoveItem(__().box.int(6));
TEST_ExpectTrue(class'MockItem'.default.objectCount == 7);
Issue("Rewriting values with `SetItem()` incorrectly handles"
@ "managed items.");
array.SetItem(__().box.int(1), __().ref.int(28347));
array.SetItem(__().box.int(4), __().ref.float(13.4));
array.SetItem(__().box.int(7), __().ref.byte(94));
TEST_ExpectTrue(class'MockItem'.default.objectCount == 5);
Issue("Rewriting values with `CreateItem()` incorrectly handles"
@ "managed items.");
array.CreateItem(__().box.int(2), class'IntRef');
array.CreateItem(__().box.int(5), class'StringRef');
array.CreateItem(__().box.int(8), class'IntArrayBox');
TEST_ExpectTrue(class'MockItem'.default.objectCount == 3);
}
protected static function Test_DeallocationHandling()
{
local MockItem exampleItem;
local AssociativeArray array;
class'MockItem'.default.objectCount = 0;
exampleItem = NewMockItem();
array = AssociativeArray(__().memory.Allocate(class'AssociativeArray'));
array.SetItem(__().box.int(-34), exampleItem, true);
array.SetItem(__().box.int(-7), exampleItem, true);
array.SetItem(__().box.int(23), NewMockItem());
array.SetItem(__().box.int(242), NewMockItem(), true);
array.CreateItem(__().box.int(24532), class'MockItem');
Context("Testing how `AssociativeArray` deals with external deallocation of"
@ "managed objects.");
Issue("`AssociativeArray` does not return `none` even though stored object"
@ "was already deallocated.");
array.GetItem(__().box.int(23)).FreeSelf();
TEST_ExpectTrue(class'MockItem'.default.objectCount == 3);
TEST_ExpectTrue(array.GetItem(__().box.int(23)) == none);
Issue("Managed items are not deallocated when they are duplicated inside"
@ "`AssociativeArray`, but they should.");
array.RemoveItem(__().box.int(-7));
// At this point we got rid of all the managed objects that were generated
// in `array` + deallocated `exampleObject`.
TEST_ExpectTrue(class'MockItem'.default.objectCount == 2);
TEST_ExpectTrue(array.GetItem(__().box.int(-34)) == none);
}
protected static function Test_Take()
{
local AssociativeArray.Entry entry;
local AssociativeArray array;
class'MockItem'.default.objectCount = 0;
array = AssociativeArray(__().memory.Allocate(class'AssociativeArray'));
array.SetItem(__().box.int(0), NewMockItem());
array.SetItem(__().box.int(1), NewMockItem());
array.SetItem(__().box.int(2), NewMockItem());
array.SetItem(__().box.int(3), NewMockItem(), true);
array.SetItem(__().box.int(4), NewMockItem(), true);
array.SetItem(__().box.int(5), NewMockItem(), true);
array.CreateItem(__().box.int(6), class'MockItem');
array.CreateItem(__().box.int(7), class'MockItem');
array.CreateItem(__().box.int(8), class'MockItem');
Context("Testing `TakeItem()` method of `AssociativeArray`.");
Issue("`TakeItem()` returns incorrect value.");
TEST_ExpectTrue(array.TakeItem(__().box.int(0)).class == class'MockItem');
TEST_ExpectTrue(array.TakeItem(__().box.int(3)).class == class'MockItem');
TEST_ExpectTrue(array.TakeItem(__().box.int(6)).class == class'MockItem');
Issue("`TakeEntry()` returns incorrect value.");
entry = array.TakeEntry(__().box.int(4));
TEST_ExpectTrue(entry.key.class == class'IntBox');
TEST_ExpectTrue(entry.value.class == class'MockItem');
entry = array.TakeEntry(__().box.int(7));
TEST_ExpectTrue(entry.key.class == class'IntBox');
TEST_ExpectTrue(entry.value.class == class'MockItem');
Issue("Objects returned by `Take()` and `takeEntry()` are still managed by"
@ "`AssociativeArray`.");
array.Empty();
TEST_ExpectTrue(class'MockItem'.default.objectCount == 7);
}
protected static function Test_LargeArray()
{
local int i;
local AcediaObject nextKey;
local AssociativeArray array;
Context("Testing storing large amount of elements in `AssociativeArray`.");
Issue("`AssociativeArray` cannot handle large amount of elements.");
array = AssociativeArray(__().memory.Allocate(class'AssociativeArray'));
for (i = 0; i < 2500; i += 1) {
if (i % 2 == 0) {
nextKey = __().text.FromString("var" @ i);
}
else {
nextKey = __().box.int(i * 56 - 435632);
}
array.SetItem(nextKey, __().ref.int(i));
}
for (i = 0; i < 2500; i += 1) {
if (i % 2 == 0) {
nextKey = __().text.FromString("var" @ i);
}
else {
nextKey = __().box.int(i * 56 - 435632);
}
TEST_ExpectTrue(IntRef(array.GetItem(nextKey)).Get() == i);
}
}
defaultproperties
{
caseGroup = "Collections"
caseName = "AssociativeArray"
}

322
sources/Data/Collections/Tests/TEST_DynamicArray.uc

@ -0,0 +1,322 @@
/**
* Set of tests for `DynamicArray` class.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_DynamicArray extends TestCase
abstract;
protected static function TESTS()
{
Test_GetSet();
Test_CreateItem();
Test_Length();
Test_Empty();
Test_AddInsert();
Test_Remove();
Test_Find();
Test_Managed();
Test_DeallocationHandling();
Test_Take();
}
protected static function Test_GetSet()
{
local DynamicArray array;
array = DynamicArray(__().memory.Allocate(class'DynamicArray'));
Context("Testing getters and setters for items of `DynamicArray`.");
Issue("Setters do not correctly expand `DynamicArray`.");
array.SetItem(0, __().box.int(-9)).SetItem(2, __().text.FromString("text"));
TEST_ExpectTrue(array.GetLength() == 3);
TEST_ExpectTrue(IntBox(array.GetItem(0)).Get() == -9);
TEST_ExpectNone(array.GetItem(1));
TEST_ExpectTrue(Text(array.GetItem(2)).ToPlainString() == "text");
Issue("Setters do not correctly overwrite items of `DynamicArray`.");
array.SetItem(1, __().box.float(34.76));
array.SetItem(2, none);
TEST_ExpectTrue(array.GetLength() == 3);
TEST_ExpectTrue(IntBox(array.GetItem(0)).Get() == -9);
TEST_ExpectTrue(FloatBox(array.GetItem(1)).Get() == 34.76);
TEST_ExpectNone(array.GetItem(2));
}
protected static function Test_CreateItem()
{
local DynamicArray array;
array = DynamicArray(__().memory.Allocate(class'DynamicArray'));
Context("Testing creating brand new items for `DynamicArray`.");
Issue("`CreateItem()` incorrectly adds new values to the"
@ "`DynamicArray`.");
array.CreateItem(1, class'Text');
array.CreateItem(3, class'IntRef');
array.CreateItem(4, class'BoolBox');
TEST_ExpectNone(array.GetItem(0));
TEST_ExpectNone(array.GetItem(2));
TEST_ExpectTrue(Text(array.GetItem(1)).ToPlainString() == "");
TEST_ExpectTrue(IntRef(array.GetItem(3)).Get() == 0);
TEST_ExpectFalse(BoolBox(array.GetItem(4)).Get());
Issue("`CreateItem()` incorrectly overrides existing values in the"
@ "`DynamicArray`.");
array.SetItem(5, __().ref.int(7));
array.CreateItem(5, class'StringRef');
TEST_ExpectTrue(StringRef(array.GetItem(5)).Get() == "");
class'MockItem'.default.objectCount = 0;
Issue("`CreateItem()` creates new object even if it cannot be recorded at"
@ "a given index.");
array.CreateItem(-1, class'MockItem');
TEST_ExpectTrue(class'MockItem'.default.objectCount == 0);
}
protected static function Test_Length()
{
local DynamicArray array;
array = DynamicArray(__().memory.Allocate(class'DynamicArray'));
Context("Testing length getter and setter for `DynamicArray`.");
Issue("Length of just created `DynamicArray` is not zero.");
TEST_ExpectTrue(array.GetLength() == 0);
Issue("`SetLength()` incorrectly changes length of the `DynamicArray`.");
array.SetLength(200).SetItem(198, __().box.int(25));
TEST_ExpectTrue(array.GetLength() == 200);
TEST_ExpectTrue(IntBox(array.GetItem(198)).Get() == 25);
array.SetLength(0);
TEST_ExpectTrue(array.GetLength() == 0);
Issue("Shrinking size of `DynamicArray` does not remove recorded items.");
array.SetLength(1000);
TEST_ExpectNone(array.GetItem(198));
}
protected static function Test_Empty()
{
local DynamicArray array;
array = DynamicArray(__().memory.Allocate(class'DynamicArray'));
Context("Testing emptying `DynamicArray`.");
array.AddItem(__().box.int(1)).AddItem(__().box.int(3))
.AddItem(__().box.int(1)).AddItem(__().box.int(3));
Issue("`Empty()` does not produce an empty array.");
array.Empty();
TEST_ExpectTrue(array.GetLength() == 0);
}
protected static function Test_AddInsert()
{
local DynamicArray array;
array = DynamicArray(__().memory.Allocate(class'DynamicArray'));
Context("Testing adding new items to `DynamicArray`.");
Issue("`Add()`/`AddItem()` incorrectly add new items to"
@ "the `DynamicArray`.");
array.AddItem(__().box.int(3)).Add(3).AddItem(__().box.byte(7)).Add(1);
TEST_ExpectTrue(array.GetLength() == 6);
TEST_ExpectNotNone(IntBox(array.GetItem(0)));
TEST_ExpectTrue(IntBox(array.GetItem(0)).Get() == 3);
TEST_ExpectNone(array.GetItem(1));
TEST_ExpectNone(array.GetItem(2));
TEST_ExpectNone(array.GetItem(3));
TEST_ExpectNotNone(ByteBox(array.GetItem(4)));
TEST_ExpectTrue(ByteBox(array.GetItem(4)).Get() == 7);
TEST_ExpectNone(array.GetItem(5));
Issue("`Insert()`/`InsertItem()` incorrectly add new items to"
@ "the `DynamicArray`.");
array.Insert(2, 2).InsertItem(0, __().ref.bool(true));
TEST_ExpectTrue(array.GetLength() == 9);
TEST_ExpectNotNone(BoolRef(array.GetItem(0)));
TEST_ExpectTrue(BoolRef(array.GetItem(0)).Get());
TEST_ExpectNotNone(IntBox(array.GetItem(1)));
TEST_ExpectTrue(IntBox(array.GetItem(1)).Get() == 3);
TEST_ExpectNone(array.GetItem(2));
TEST_ExpectNone(array.GetItem(6));
TEST_ExpectNotNone(ByteBox(array.GetItem(7)));
TEST_ExpectTrue(ByteBox(array.GetItem(7)).Get() == 7);
TEST_ExpectNone(array.GetItem(8));
}
protected static function Test_Remove()
{
local DynamicArray array;
array = DynamicArray(__().memory.Allocate(class'DynamicArray'));
Context("Testing removing items from `DynamicArray`.");
array.AddItem(__().box.int(1)).AddItem(__().box.int(3))
.AddItem(__().box.int(1)).AddItem(__().box.int(3))
.AddItem(__().box.int(5)).AddItem(__().box.int(2))
.AddItem(__().box.int(4)).AddItem(__().box.int(7))
.AddItem(__().box.int(5)).AddItem(__().box.int(1))
.AddItem(__().box.int(5)).AddItem(__().box.int(0));
Issue("`Remove()` incorrectly removes items from array.");
array.Remove(3, 2).Remove(0, 2).Remove(7, 9);
TEST_ExpectTrue(array.GetLength() == 7);
TEST_ExpectTrue(IntBox(array.GetItem(0)).Get() == 1);
TEST_ExpectTrue(IntBox(array.GetItem(1)).Get() == 2);
TEST_ExpectTrue(IntBox(array.GetItem(2)).Get() == 4);
TEST_ExpectTrue(IntBox(array.GetItem(3)).Get() == 7);
TEST_ExpectTrue(IntBox(array.GetItem(4)).Get() == 5);
TEST_ExpectTrue(IntBox(array.GetItem(5)).Get() == 1);
TEST_ExpectTrue(IntBox(array.GetItem(6)).Get() == 5);
Issue("`RemoveItem()` incorrectly removes items from array.");
array.RemoveItem(__().box.int(1)).RemoveItem(__().box.int(5), true);
TEST_ExpectTrue(array.GetLength() == 4);
TEST_ExpectTrue(IntBox(array.GetItem(0)).Get() == 2);
TEST_ExpectTrue(IntBox(array.GetItem(1)).Get() == 4);
TEST_ExpectTrue(IntBox(array.GetItem(2)).Get() == 7);
TEST_ExpectTrue(IntBox(array.GetItem(3)).Get() == 5);
Issue("`RemoveIndex()` incorrectly removes items from array.");
array.RemoveIndex(0).RemoveIndex(1).RemoveIndex(1);
TEST_ExpectTrue(array.GetLength() == 1);
TEST_ExpectTrue(IntBox(array.GetItem(0)).Get() == 4);
}
protected static function Test_Find()
{
local DynamicArray array;
array = DynamicArray(__().memory.Allocate(class'DynamicArray'));
Context("Testing searching for items in `DynamicArray`.");
array.AddItem(__().box.int(1)).AddItem(__().box.int(3))
.AddItem(__().box.int(1)).AddItem(__().box.int(3))
.AddItem(__().box.int(5)).AddItem(__().box.bool(true))
.AddItem(none).AddItem(__().box.float(72.54))
.AddItem(__().box.int(5)).AddItem(__().box.int(1))
.AddItem(__().box.int(5)).AddItem(__().box.int(0));
Issue("`Find()` does not properly find indices of existing items.");
TEST_ExpectTrue(array.Find(__().box.int(5)) == 4);
TEST_ExpectTrue(array.Find(__().box.int(1)) == 0);
TEST_ExpectTrue(array.Find(__().box.int(0)) == 11);
TEST_ExpectTrue(array.Find(__().box.float(72.54)) == 7);
TEST_ExpectTrue(array.Find(__().box.bool(true)) == 5);
TEST_ExpectTrue(array.Find(none) == 6);
Issue("`Find()` does not return `-1` on missing values.");
TEST_ExpectTrue(array.Find(__().box.int(42)) == -1);
TEST_ExpectTrue(array.Find(__().box.float(72.543)) == -1);
TEST_ExpectTrue(array.Find(__().box.bool(false)) == -1);
TEST_ExpectTrue(array.Find(__().box.byte(128)) == -1);
}
protected static function MockItem NewMockItem()
{
return MockItem(__().memory.Allocate(class'MockItem'));
}
// Creates array with mock objects, also zeroing their count.
// Managed items' count: 6, but 12 items total.
protected static function DynamicArray NewMockArray()
{
local DynamicArray array;
class'MockItem'.default.objectCount = 0;
array = DynamicArray(__().memory.Allocate(class'DynamicArray'));
array.AddItem(NewMockItem(), true).AddItem(NewMockItem(), false)
.InsertItem(2, NewMockItem(), true).AddItem(NewMockItem(), true)
.AddItem(NewMockItem(), false).AddItem(NewMockItem(), true)
.InsertItem(6, NewMockItem(), false).AddItem(NewMockItem(), true)
.InsertItem(3, NewMockItem(), false).AddItem(NewMockItem(), false)
.InsertItem(10, NewMockItem(), true).AddItem(NewMockItem(), false);
return array;
}
protected static function Test_Managed()
{
local MockItem exampleItem;
local DynamicArray array;
exampleItem = NewMockItem();
// Managed items' count: 6, but 12 items total.
array = NewMockArray();
Context("Testing how `DynamicArray` deallocates managed objects.");
Issue("`Remove()` incorrectly deallocates managed items.");
// -2 managed items
array.Remove(3, 2).Remove(0, 2).Remove(7, 9);
TEST_ExpectTrue(class'MockItem'.default.objectCount == 10);
Issue("`RemoveIndex()` incorrectly deallocates managed items.");
// -1 managed items
array.RemoveIndex(3).RemoveIndex(0);
TEST_ExpectTrue(class'MockItem'.default.objectCount == 9);
// -1 managed items
array.RemoveItem(exampleItem, true).RemoveItem(exampleItem, true);
TEST_ExpectTrue(class'MockItem'.default.objectCount == 8);
// -2 managed items
array.RemoveItem(exampleItem);
TEST_ExpectTrue(class'MockItem'.default.objectCount == 6);
array = NewMockArray();
Issue("Shrinking array with `SetLength()` incorrectly handles"
@ "managed items");
// -4 managed items
array.SetLength(3);
TEST_ExpectTrue(class'MockItem'.default.objectCount == 8);
Issue("Rewriting values with `SetItem()` incorrectly handles"
@ "managed items.");
// -2 managed items
array.SetItem(0, exampleItem, true);
array.SetItem(2, exampleItem, true);
TEST_ExpectTrue(class'MockItem'.default.objectCount == 6);
}
protected static function Test_DeallocationHandling()
{
local MockItem exampleItem;
local DynamicArray array;
exampleItem = NewMockItem();
// Managed items' count: 6, but 12 items total.
array = NewMockArray();
Context("Testing how `DynamicArray` deals with external deallocation of"
@ "managed objects.");
Issue("`DynamicArray` does not return `none` even though stored object"
@ "was already deallocated.");
array.GetItem(0).FreeSelf();
TEST_ExpectTrue(class'MockItem'.default.objectCount == 11);
TEST_ExpectTrue(array.GetItem(0) == none);
Issue("Managed items are not deallocated when they are duplicated inside"
@ "`DynamicArray`, but they should.");
array.SetItem(1, exampleItem, true).SetItem(2, exampleItem, true);
TEST_ExpectTrue(class'MockItem'.default.objectCount == 10);
array.SetLength(2);
// At this point we got rid of all the managed objects that were generated
// in `array` + deallocated `exampleObject`.
TEST_ExpectTrue(class'MockItem'.default.objectCount == 5);
TEST_ExpectTrue(array.GetItem(1) == none);
}
protected static function Test_Take()
{
local DynamicArray array;
Context("Testing `TakeItem()` method of `DynamicArray`.");
// Managed items' count: 6, but 12 items total.
array = NewMockArray();
Issue("`TakeItem()` returns incorrect value.");
TEST_ExpectTrue(array.TakeItem(0).class == class'MockItem');
TEST_ExpectTrue(array.TakeItem(3).class == class'MockItem');
TEST_ExpectTrue(array.TakeItem(4).class == class'MockItem');
TEST_ExpectTrue(array.TakeItem(6).class == class'MockItem');
Issue("Objects returned by `TakeItem()` are still managed by"
@ "`DynamicArray`.");
array.Empty();
TEST_ExpectTrue(class'MockItem'.default.objectCount == 9);
}
defaultproperties
{
caseGroup = "Collections"
caseName = "DynamicArray"
}

142
sources/Data/Collections/Tests/TEST_Iterator.uc

@ -0,0 +1,142 @@
/**
* Set of tests for `Iterator` classes.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_Iterator extends TestCase
abstract;
var const int TESTED_ITEMS_AMOUNT;
var array<AcediaObject> items;
var array<byte> seenFlags;
protected static function CreateItems()
{
local int i;
ResetFlags();
for (i = 0; i < default.TESTED_ITEMS_AMOUNT; i += 1) {
default.items[default.items.length] = __().ref.float(i*2 + 1/i);
}
}
protected static function ResetFlags()
{
default.seenFlags.length = 0;
default.seenFlags.length = default.TESTED_ITEMS_AMOUNT;
}
protected static function DoTestIterator(Iter iter)
{
local int i;
local int seenCount;
local AcediaObject nextObject;
ResetFlags();
while (!iter.HasFinished())
{
nextObject = iter.Get();
for (i = 0; i < default.items.length; i += 1)
{
if (default.items[i] == nextObject)
{
if (default.seenFlags[i] == 0) {
seenCount += 1;
}
default.seenFlags[i] = 1;
continue;
}
}
iter.Next();
}
TEST_ExpectTrue(seenCount == default.TESTED_ITEMS_AMOUNT);
}
protected static function TESTS()
{
// Prepare
CreateItems();
// Test
Test_DynamicArray();
Test_AssociativeArray();
}
protected static function Test_DynamicArray()
{
local int i;
local Iter iter;
local DynamicArray array;
array = DynamicArray(__().memory.Allocate(class'DynamicArray'));
iter = array.Iterate();
Context("Testing iterator for `DynamicArray`");
Issue("`DynamicArray` returns `none` iterator.");
TEST_ExpectNotNone(iter);
Issue("Iterator for empty `DynamicArray` is not finished by default.");
TEST_ExpectTrue(iter.HasFinished());
Issue("Iterator for empty `DynamicArray` does not return `none` as"
@ "a current item.");
TEST_ExpectNone(iter.Get());
TEST_ExpectNone(iter.Next().Get());
for (i = 0; i < default.items.length; i += 1) {
array.AddItem(default.items[i]);
}
iter = array.Iterate();
Issue("`DynamicArray` returns `none` iterator.");
TEST_ExpectNotNone(iter);
Issue("`DynamicArray`'s iterator iterates over incorrect set of items.");
DoTestIterator(iter);
}
protected static function Test_AssociativeArray()
{
local int i;
local Iter iter;
local AssociativeArray array;
array = AssociativeArray(__().memory.Allocate(class'AssociativeArray'));
iter = array.Iterate();
Context("Testing iterator for `AssociativeArray`");
Issue("`AssociativeArray` returns `none` iterator.");
TEST_ExpectNotNone(iter);
Issue("Iterator for empty `AssociativeArray` is not finished by default.");
TEST_ExpectTrue(iter.HasFinished());
Issue("Iterator for empty `AssociativeArray` does not return `none` as"
@ "a current item.");
TEST_ExpectNone(iter.Get());
TEST_ExpectNone(iter.Next().Get());
for (i = 0; i < default.items.length; i += 1) {
array.SetItem(__().box.int(i), default.items[i]);
}
iter = array.Iterate();
Issue("`AssociativeArray` returns `none` iterator.");
TEST_ExpectNotNone(iter);
Issue("`AssociativeArray`'s iterator iterates over incorrect set of"
@ "items.");
DoTestIterator(iter);
}
defaultproperties
{
caseGroup = "Collections"
caseName = "Iterator"
TESTED_ITEMS_AMOUNT = 100
}

948
sources/Data/JSON/JArray.uc

@ -1,948 +0,0 @@
/**
* This class implements JSON array storage capabilities.
* Array stores ordered JSON values that can be referred by their index.
* It can contain any mix of JSON value types and cannot have any gaps,
* i.e. in array of length N, there must be a valid value for all indices
* from 0 to N-1. Values of the array can beL
* ~ Boolean, string, null or number (float in this implementation) data;
* ~ Other JSON Arrays;
* ~ Other JSON objects (see `JObject` class).
*
* This implementation provides a variety of functionality,
* including parsing, displaying, getters and setters for JSON types that
* allow to freely set and fetch their values by index.
* JSON objects and arrays can be fetched by getters, but you cannot
* add existing object or array to another object. Instead one has to either
* clone existing object or create an empty one and then manually fill
* with data.
* This allows to avoid loop situations, where object is
* contained in itself.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class JArray extends JSON;
// Data will be stored as an array of JSON values
var private array<JStorageAtom> data;
/**
* Returns type (`JType`) of a property with a index in our collection.
*
* @param index Index of the JSON value to get the type of.
* @return Type of the property at the index `index`.
* `JSON_Undefined` iff element at that index does not exist.
*/
public final function JType GetTypeOf(int index)
{
if (index < 0) return JSON_Undefined;
if (index >= data.length) return JSON_Undefined;
return data[index].type;
}
/**
* Returns current length of the caller array.
*
* @return Length (amount of elements) in the caller array.
* Means that max index with recorded value is `GetLength() - 1`
* (min index is `0`).
*/
public final function int GetLength()
{
return data.length;
}
/**
* Changes length of the caller `JArray`.
*
* If length is decreased - variables that fit into new length will be
* preserved, others - erased.
* In case of the increase - sets values at new indices to "null".
*
* @param newLength New length of the caller `JArray`.
* Negative values will be treated as zero.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function SetLength(int newLength)
{
local int i;
local int oldLength;
newLength = Max(0, newLength);
oldLength = data.length;
data.length = newLength;
if (oldLength >= newLength) {
return;
}
i = oldLength;
while (i < newLength)
{
SetNull(i);
i += 1;
}
}
/**
* Gets the value (as a `float`) at the index `index`, assuming it has
* `JSON_Number` type.
*
* Forms a pair with `GetInteger()` method. JSON allows to specify
* arbitrary precision for the number variables, but UnrealScript can only
* store a limited range of numeric value.
* To alleviate this problem we store numeric JSON values as both
* `float` and `int` and can return either of the requested versions.
*
* @param index Index of the value to get;
* must be between 0 and `GetLength() - 1` inclusively.
* @param defaultValue Value to return if element does not exist or
* has a different type (can be checked by `GetTypeOf()`).
* @return Number value of the element at the index `index`,
* if it exists and has `JSON_Number` type.
* Otherwise returns passed `defaultValue`.
*/
public final function float GetNumber(int index, optional float defaultValue)
{
if (index < 0) return defaultValue;
if (index >= data.length) return defaultValue;
if (data[index].type != JSON_Number) return defaultValue;
return data[index].numberValue;
}
/**
* Gets the value (as an `int`) at the index `index`, assuming it has
* `JSON_Number` type.
*
* Forms a pair with `GetInteger()` method. JSON allows to specify
* arbitrary precision for the number variables, but UnrealScript can only
* store a limited range of numeric value.
* To alleviate this problem we store numeric JSON values as both
* `float` and `int` and can return either of the requested versions.
*
* @param index Index of the value to get;
* must be between 0 and `GetLength() - 1` inclusively.
* @param defaultValue Value to return if element does not exist or
* has a different type (can be checked by `GetTypeOf()`).
* @return Number value of the element at the index `index`,
* if it exists and has `JSON_Number` type.
* Otherwise returns passed `defaultValue`.
*/
public final function float GetInteger(int index, optional float defaultValue)
{
if (index < 0) return defaultValue;
if (index >= data.length) return defaultValue;
if (data[index].type != JSON_Number) return defaultValue;
return data[index].numberValueAsInt;
}
/**
* Gets the value at the index `index`, assuming it has `JSON_String` type.
*
* See also `GetClass()` method.
*
* @param index Index of the value to get;
* must be between 0 and `GetLength() - 1` inclusively.
* @param defaultValue Value to return if element does not exist or
* has a different type (can be checked by `GetTypeOf()`).
* @return String value of the element at the index `index`,
* if it exists and has `JSON_String` type.
* Otherwise returns passed `defaultValue`.
*/
public final function string GetString(int index, optional string defaultValue)
{
if (index < 0) return defaultValue;
if (index >= data.length) return defaultValue;
if (data[index].type != JSON_String) return defaultValue;
return data[index].stringValue;
}
/**
* Gets the value at the index `index` as a `class`, assuming it has
* `JSON_String` type.
*
* JSON does not support to store class data type, but we can use string type
* for that. This method attempts to load a class object from it's full name,
* (like `Engine.Actor`) recorded inside an appropriate string value.
*
* @param index Index of the value to get;
* must be between 0 and `GetLength() - 1` inclusively.
* @param defaultValue Value to return if element does not exist,
* has a different type (can be checked by `GetTypeOf()`) or not
* a valid class name.
* @return Class value of the element at the index `index`,
* if it exists, has `JSON_String` type and it represents
* a full name of some class.
* Otherwise returns passed `defaultValue`.
*/
public final function class<Object> GetClass(
int index,
optional class<Object> defaultValue)
{
if (index < 0) return defaultValue;
if (index >= data.length) return defaultValue;
if (data[index].type != JSON_String) return defaultValue;
TryLoadingStringAsClass(data[index]);
if (data[index].stringValueAsClass != none) {
return data[index].stringValueAsClass;
}
return defaultValue;
}
/**
* Gets the value at the index `index`, assuming it has `JSON_Boolean` type.
*
* See also `GetClass()` method.
*
* @param index Index of the value to get;
* must be between 0 and `GetLength() - 1` inclusively.
* @param defaultValue Value to return if property does not exist or
* has a different type (can be checked by `GetTypeOf()`).
* @return String value of the element at the index `index`,
* if it exists and has `JSON_Boolean` type.
* Otherwise returns passed `defaultValue`.
*/
public final function bool GetBoolean(int index, optional bool defaultValue)
{
if (index < 0) return defaultValue;
if (index >= data.length) return defaultValue;
if (data[index].type != JSON_Boolean) return defaultValue;
return data[index].booleanValue;
}
/**
* Checks if an array element at the index `index` has `JSON_Null` type.
*
* Alternatively consider using `GetType()` method.
*
* @param index Index of the element to check for being `null`.
* @return `true` if element at the given index exists and
* has type `JSON_Null`; `false` otherwise.
*/
public final function bool IsNull(int index)
{
if (index < 0) return false;
if (index >= data.length) return false;
return (data[index].type == JSON_Null);
}
/**
* Gets the value at the index `index`, assuming it has `JSON_Array` type.
*
* @param index Index of the value to check for being "null";
* must be between 0 and `GetLength() - 1` inclusively.
* @return `JArray` object value at the given index, if it exists and
* has `JSON_Array` type.
* Otherwise returns `none`.
*/
public final function JArray GetArray(int index)
{
if (index < 0) return none;
if (index >= data.length) return none;
if (data[index].type != JSON_Array) return none;
return JArray(data[index].complexValue);
}
/**
* Gets the value at the index `index`, assuming it has `JSON_Object` type.
*
* @param index Index of the value to check for being "null";
* must be between 0 and `GetLength() - 1` inclusively.
* @return `JObject` object value at the given index, if it exists and
* has `JSON_Array` type.
* Otherwise returns `none`.
*/
public final function JObject GetObject(int index)
{
if (index < 0) return none;
if (index >= data.length) return none;
if (data[index].type != JSON_Object) return none;
return JObject(data[index].complexValue);
}
/**
* Sets the number value (as `float`) at the index `index`, erasing previous
* value (if it was recorded).
*
* If negative index is given - does nothing.
* If given index is too large (`>= GetLength()`) then array will be
* extended, setting values at new indices (except specified `index`)
* to "null" value (`JSON_Null`).
*
* Forms a pair with `SetInteger()` method.
* While JSON standard allows to store numbers with arbitrary precision,
* UnrealScript's types have a limited range.
* To alleviate this problem we store numbers in both `float`- and
* `int`-type variables to extended supported range of values.
* So if you need to store a number with fractional part, you should
* prefer `SetNumber()` and for integer values `SetInteger()` is preferable.
* Both will record a value of type `JSON_Number`.
*
* @param index Index at which to set given numeric value.
* @param value Value to set at given index.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray SetNumber(int index, float value)
{
local JStorageAtom newStorageValue;
if (index < 0) return self;
if (index >= data.length) {
SetLength(index + 1);
}
newStorageValue.type = JSON_Number;
newStorageValue.numberValue = value;
newStorageValue.numberValueAsInt = int(value);
newStorageValue.preferIntegerValue = false;
data[index] = newStorageValue;
return self;
}
/**
* Sets the number value (as `int`) at the index `index`, erasing previous
* value (if it was recorded).
*
* If negative index is given - does nothing.
* If given index is too large (`>= GetLength()`) then array will be
* extended, setting values at new indices (except specified `index`)
* to "null" value (`JSON_Null`).
*
* Forms a pair with `SetNumber()` method.
* While JSON standard allows to store numbers with arbitrary precision,
* UnrealScript's types have a limited range.
* To alleviate this problem we store numbers in both `float`- and
* `int`-type variables to extended supported range of values.
* So if you need to store a number with fractional part, you should
* prefer `SetNumber()` and for integer values `SetInteger()` is preferable.
* Both will record a value of type `JSON_Number`.
*
* @param index Index at which to set given numeric value.
* @param value Value to set at given index.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray SetInteger(int index, int value)
{
local JStorageAtom newStorageValue;
if (index < 0) return self;
if (index >= data.length) {
SetLength(index + 1);
}
newStorageValue.type = JSON_Number;
newStorageValue.numberValue = float(value);
newStorageValue.numberValueAsInt = value;
newStorageValue.preferIntegerValue = true;
data[index] = newStorageValue;
return self;
}
/**
* Sets the string value at the given index `index`, erasing previous value
* (if it was recorded).
*
* If negative index is given - does nothing.
* If given index is too large (`>= GetLength()`) then array will be
* extended, setting values at new indices (except specified `index`)
* to "null" value (`JSON_Null`).
*
* Also see `SetClass()` method.
*
* @param index Index at which to set given string value.
* @param value Value to set at given index.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray SetString(int index, string value)
{
local JStorageAtom newStorageValue;
if (index < 0) return self;
if (index >= data.length) {
SetLength(index + 1);
}
newStorageValue.type = JSON_String;
newStorageValue.stringValue = value;
data[index] = newStorageValue;
return self;
}
/**
* Sets the string value, corresponding to a given class `value`,
* at the index `index`, erasing previous value (if it was recorded).
*
* Value in question will have `JSON_String` type.
*
* If negative index is given - does nothing.
* If given index is too large (`>= GetLength()`) then array will be
* extended, setting values at new indices (except specified `index`)
* to "null" value (`JSON_Null`).
*
* We want to allow storing `class` data in our JSON containers, but JSON
* standard does not support such a type, so we have to use string type
* to store `class`' name instead.
* Also see `GetClass()` method`.
*
* @param index Index at which to set given value.
* @param value Value to set at given index.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray SetClass(int index, class<Object> value)
{
local JStorageAtom newStorageValue;
if (index < 0) return self;
if (index >= data.length) {
SetLength(index + 1);
}
newStorageValue.type = JSON_String;
newStorageValue.stringValue = string(value);
newStorageValue.stringValueAsClass = value;
data[index] = newStorageValue;
return self;
}
/**
* Sets the boolean value at the given index `index`, erasing previous value
* (if it was recorded).
*
* If negative index is given - does nothing.
* If given index is too large (`>= GetLength()`) then array will be
* extended, setting values at new indices (except specified `index`)
* to "null" value (`JSON_Null`).
*
* @param index Index at which to set given boolean value.
* @param value Value to set at given index.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray SetBoolean(int index, bool value)
{
local JStorageAtom newStorageValue;
if (index < 0) return self;
if (index >= data.length) {
SetLength(index + 1);
}
newStorageValue.type = JSON_Boolean;
newStorageValue.booleanValue = value;
data[index] = newStorageValue;
return self;
}
/**
* Sets the value at the given index `index` to be "null" (`JSON_Null`),
* erasing previous value (if it was recorded).
*
* If negative index is given - does nothing.
* If given index is too large (`>= GetLength()`) then array will be
* extended, setting values at new indices (except specified `index`)
* to "null" value (`JSON_Null`).
*
* @param index Index at which to set "null" value to.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray SetNull(int index)
{
local JStorageAtom newStorageValue;
if (index < 0) return self;
if (index >= data.length) {
SetLength(index + 1);
}
newStorageValue.type = JSON_Null;
data[index] = newStorageValue;
return self;
}
/**
* Sets the value at the given index `index` to store `JArray` object
* (JSON array type).
*
* If negative index is given - does nothing.
* If given index is too large (`>= GetLength()`) then array will be
* extended, setting values at new indices (except specified `index`)
* to "null" value (`JSON_Null`).
*
* NOTE: This method DOES NOT make caller `JArray` store a
* given reference, instead it clones it (see `Clone()`) into a new copy and
* stores that. This is made this way to ensure you can not, say, store
* an object in itself or it's children.
* See also `CreateArray()` method.
*
* @param index Index at which to set given array value.
* @param template Template `JArray` to clone.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray SetArray(int index, JArray template)
{
local JStorageAtom newStorageValue;
if (index < 0) return self;
if (template == none) return self;
if (index >= data.length) {
SetLength(index + 1);
}
newStorageValue.type = JSON_Array;
newStorageValue.complexValue = template.Clone();
data[index] = newStorageValue;
return self;
}
/**
* Sets the value at the given index `index` to store `JObject` object
* (JSON object type).
*
* If negative index is given - does nothing.
* If given index is too large (`>= GetLength()`) then array will be
* extended, setting values at new indices (except specified `index`)
* to "null" value (`JSON_Null`).
*
* NOTE: This method DOES NOT make caller `JArray` store a
* given reference, instead it clones it (see `Clone()`) into a new copy and
* stores that. This is made this way to ensure you can not, say, store
* an object in itself or it's children.
* See also `CreateObject()` method.
*
* @param index Index at which to set given array value.
* @param template Template `JObject` to clone.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray SetObject(int index, JObject template)
{
local JStorageAtom newStorageValue;
if (index < 0) return self;
if (template == none) return self;
if (index >= data.length) {
SetLength(index + 1);
}
newStorageValue.type = JSON_Object;
newStorageValue.complexValue = template.Clone();
data[index] = newStorageValue;
return self;
}
/**
* Sets the value oat the given index `index` to store a new
* `JArray` object (JSON array type).
*
* See also `SetArray()` method.
*
* If negative index is given - does nothing.
* If given index is too large (`>= GetLength()`) then array will be
* extended, setting values at new indices (except specified `index`)
* to "null" value (`JSON_Null`).
*
* @param index Index at which to create a new `JArray`.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray CreateArray(int index)
{
local JStorageAtom newStorageValue;
if (index < 0) return self;
if (index >= data.length) {
SetLength(index + 1);
}
newStorageValue.type = JSON_Array;
newStorageValue.complexValue = _.json.newArray();
data[index] = newStorageValue;
return self;
}
/**
* Sets the value oat the given index `index` to store a new
* `JObject` object (JSON object type).
*
* See also `SetObject()` method.
*
* If negative index is given - does nothing.
* If given index is too large (`>= GetLength()`) then array will be
* extended, setting values at new indices (except specified `index`)
* to "null" value (`JSON_Null`).
*
* @param index Index at which to create a new `JObject`.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray CreateObject(int index)
{
local JStorageAtom newStorageValue;
if (index < 0) return self;
if (index >= data.length) {
SetLength(index + 1);
}
newStorageValue.type = JSON_Object;
newStorageValue.complexValue = _.json.newObject();
data[index] = newStorageValue;
return self;
}
/**
* Appends numeric value (as `float`) at the end of the caller `JArray`.
*
* Forms a pair with `AddInteger()` method.
* While JSON standard allows to store numbers with arbitrary precision,
* UnrealScript's types have a limited range.
* To alleviate this problem we store numbers in both `float`- and
* `int`-type variables to extended supported range of values.
* So if you need to store a number with fractional part, you should
* prefer `AddNumber()` and for integer values `AddInteger()` is preferable.
* Both will record a value of type `JSON_Number`.
*
* @param value Numeric value to append.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray AddNumber(float value)
{
return SetNumber(data.length, value);
}
/**
* Appends numeric value (as `int`) at the end of the caller `JArray`.
*
* Forms a pair with `AddNumber()` method.
* While JSON standard allows to store numbers with arbitrary precision,
* UnrealScript's types have a limited range.
* To alleviate this problem we store numbers in both `float`- and
* `int`-type variables to extended supported range of values.
* So if you need to store a number with fractional part, you should
* prefer `AddNumber()` and for integer values `AddInteger()` is preferable.
* Both will record a value of type `JSON_Number`.
*
* @param value Numeric value to append.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray AddInteger(int value)
{
return SetInteger(data.length, value);
}
/**
* Appends string value at the end of the caller `JArray`.
*
* Also see `AddClass()` method.
*
* @param value String value to append.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray AddString(string value)
{
return SetString(data.length, value);
}
/**
* Appends string value, corresponding to a given class `value`, at the end of
* the caller `JArray`.
*
* Value in question will have `JSON_String` type.
*
* We want to allow storing `class` data in our JSON containers, but JSON
* standard does not support such a type, so we have to use string type
* to store `class`' name instead.
* Also see `GetClass()` method`.
*
* @param value Class value to append.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray AddClass(class<Object> value)
{
return SetClass(data.length, value);
}
/**
* Appends boolean value at the end of the caller `JArray`.
*
* @param value Boolean value to append.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray AddBoolean(bool value)
{
return SetBoolean(data.length, value);
}
/**
* Appends "null" value at the end of the caller `JArray`.
*
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray AddNull()
{
return SetNull(data.length);
}
/**
* Appends new empty `JArray` (JSON array type) at the end of
* the caller `JArray`.
*
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray AddArray()
{
return CreateArray(data.length);
}
/**
* Appends new empty `JObject` (JSON object type) at the end of
* the caller `JArray`.
*
* @return Reference to the caller object, to allow for function chaining.
*/
public final function JArray AddObject()
{
return CreateObject(data.length);
}
// Removes up to `amount` (minimum of `1`) of values, starting from
// a given index.
// If `index` falls outside array boundaries - nothing will be done.
// Returns `true` if value was actually removed and `false` if it didn't exist.
/**
* Removes up to `amount` (minimum of `1`) of values, starting from
* a given index `index`.
*
* If `index` falls outside array boundaries - nothing will be done.
*
* @param index Index of first value to remove.
* @param amount Amount of values to remove.
* @return Reference to the caller object, to allow for function chaining.
*/
public final function bool RemoveValue(int index, optional int amount)
{
local int i;
if (index < 0) return false;
if (index >= data.length) return false;
amount = Max(amount, 1);
amount = Min(amount, data.length - index);
for (i = index; i < index + amount; i += 1)
{
if (data[index].complexValue != none) {
data[index].complexValue.Destroy();
}
}
data.Remove(index, amount);
return true;
}
/**
* Completely clears caller `JObject` of all values.
*/
public function Clear()
{
RemoveValue(0, data.length);
}
/**
* Checks if caller JSON container's values form a subset of
* `rightJSON`'s values.
*
* @return `true` if caller ("left") object is a subset of `rightJSON`
* and `false` otherwise.
*/
public function bool IsSubsetOf(JSON rightValue)
{
local int i;
local JArray rightArray;
local array<JStorageAtom> rightAtomArray;
rightArray = JArray(rightValue);
if (rightArray == none) return false;
if (data.length > rightArray.data.length) return false;
rightAtomArray = rightArray.data;
for (i = 0; i < data.length; i += 1)
{
if (!AreAtomsEqual(data[i], rightAtomArray[i])) {
return false;
}
}
return true;
}
/**
* Makes an exact copy of the caller `JArray`
*
* @return Copy of the caller `JArray`. Guaranteed to be `JArray`
* (or `none`, if appropriate object could not be created).
*/
public function JSON Clone()
{
local int i;
local JArray clonedArray;
local array<JStorageAtom> clonedData;
clonedArray = _.json.NewArray();
if (clonedArray == none)
{
_.logger.Failure("Cannot clone `JArray`: cannot spawn a new instance.");
return none;
}
clonedData = data;
for (i = 0; i < clonedData.length; i += 1)
{
if (clonedData[i].complexValue == none) continue;
if ( clonedData[i].type != JSON_Array
&& clonedData[i].type != JSON_Object) {
continue;
}
clonedData[i].complexValue = clonedData[i].complexValue.Clone();
}
clonedArray.data = clonedData;
return clonedArray;
}
/**
* Uses given parser to parse a new array of values (append them to the end of
* the caller array) inside the caller `JArray`.
*
* Only adds new values if parsing the whole array was successful,
* otherwise even successfully parsed properties will be discarded.
*
* `parser` must point at the text describing a JSON array in
* a valid notation. Then it parses that container inside memory, but
* instead of creating it as a separate entity, adds it's values to
* the caller `JArray`.
*
* This method does not try to validate passed JSON and can accept invalid
* JSON by making some assumptions, but it is an undefined behavior and
* one should not expect it.
* Method is only guaranteed to work on valid JSON.
*
* @param parser Parser that method would use to parse `JArray` from
* wherever it left. It's confirmed will not be changed, but if parsing
* was successful, - it will point at the next available character.
* Do not treat `parser` being in a non-failed state as a confirmation of
* successful parsing: JSON parsing might fail regardless.
* Check return value for that.
* @return `true` if parsing was successful and `false` otherwise.
*/
public function bool ParseIntoSelfWith(Parser parser)
{
local bool parsingSucceeded;
local Parser.ParserState initState, confirmedState;
local JStorageAtom nextAtom;
local array<JStorageAtom> parsedAtoms;
if (parser == none) return false;
initState = parser.GetCurrentState();
confirmedState = parser.Skip().Match("[").GetCurrentState();
if (!parser.Ok())
{
parser.RestoreState(initState);
return false;
}
while (parser.Ok() && !parser.HasFinished())
{
confirmedState = parser.Skip().GetCurrentState();
if (parser.Match("]").Ok()) {
parsingSucceeded = true;
break;
}
if ( parsedAtoms.length > 0
&& !parser.RestoreState(confirmedState).Match(",").Skip().Ok()) {
break;
}
else if (parser.Ok()) {
confirmedState = parser.GetCurrentState();
}
nextAtom = ParseAtom(parser.RestoreState(confirmedState));
if (nextAtom.type == JSON_Undefined) {
break;
}
parsedAtoms[parsedAtoms.length] = nextAtom;
}
HandleParsedAtoms(parsedAtoms, parsingSucceeded);
if (!parsingSucceeded) {
parser.RestoreState(initState);
}
return parsingSucceeded;
}
// Either cleans up or adds a list of parsed values,
// depending on whether parsing was successful or not.
private function HandleParsedAtoms(
array<JStorageAtom> parsedAtoms,
bool parsingSucceeded)
{
local int i;
if (parsingSucceeded)
{
for (i = 0; i < parsedAtoms.length; i += 1) {
data[data.length] = parsedAtoms[i];
}
return;
}
for (i = 0; i < parsedAtoms.length; i += 1)
{
if (parsedAtoms[i].complexValue != none) {
parsedAtoms[i].complexValue.Destroy();
}
}
}
/**
* Displays caller `JArray` with a provided preset.
*
* See `Display()` for a simpler to use method.
*
* @param displaySettings Struct that describes precisely how to display
* caller `JArray`. Can be used to emulate `Display()` call.
* @return String representation of caller JSON container in format defined by
* `displaySettings`.
*/
public function string DisplayWith(JSONDisplaySettings displaySettings)
{
local int i;
local bool isntFirstElement;
local string contents;
local string openingBraces, closingBraces;
local string elementsSeparator;
local JSONDisplaySettings innerSettings;
if (displaySettings.stackIndentation) {
innerSettings = IndentSettings(displaySettings, true);
}
else {
innerSettings = displaySettings;
}
// Prepare delimiters using appropriate indentation rules
// We only use inner settings for the part right after '[',
// as the rest is supposed to be aligned with outer objects
openingBraces = displaySettings.beforeArrayOpening
$ "[" $ innerSettings.afterArrayOpening;
closingBraces = displaySettings.beforeArrayEnding
$ "]" $ displaySettings.afterArrayEnding;
elementsSeparator = "," $ innerSettings.afterArrayComma;
if (innerSettings.colored) {
elementsSeparator = "{$json_comma" $ elementsSeparator $ "}";
openingBraces = "{$json_arrayBraces" $ openingBraces $ "}";
closingBraces = "{$json_arrayBraces" $ closingBraces $ "}";
}
// Display inner properties
for (i = 0; i < data.length; i += 1)
{
if (isntFirstElement) {
contents $= elementsSeparator;
}
contents $= DisplayAtom(data[i], innerSettings);
isntFirstElement = true;
}
return openingBraces $ contents $ closingBraces;
}
defaultproperties
{
}

1026
sources/Data/JSON/JObject.uc

File diff suppressed because it is too large Load Diff

788
sources/Data/JSON/JSON.uc

@ -1,788 +0,0 @@
/**
* JSON is an open standard file format, and data interchange format,
* that uses human-readable text to store and transmit data objects
* consisting of name–value pairs and array data types.
* For more information refer to https://en.wikipedia.org/wiki/JSON
* This is a base class for implementation of JSON objects and arrays
* for Acedia.
*
* JSON data is stored as an object (represented via `JSONObject`) that
* contains a set of name-value pairs, where value can be
* a number, string, boolean value, another object or
* an array (represented by `JSONArray`).
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class JSON extends AcediaActor
abstract
config(AcediaSystem);
/**
* Enumeration for possible types of JSON values.
*/
enum JType
{
// Technical type, used to indicate that requested value is missing.
// Undefined values are not part of JSON format.
JSON_Undefined,
// An empty value, in teste representation defined by a single word "null".
JSON_Null,
// A number, recorded as a float.
// JSON itself doesn't specify whether number is an integer or float.
JSON_Number,
// A string.
JSON_String,
// A bool value.
JSON_Boolean,
// Array of other JSON values, stored without names;
// Single array can contain any mix of value types.
JSON_Array,
// Another JSON object, i.e. associative array of name-value pairs
JSON_Object
};
/**
* Represents a single JSON value.
*/
struct JStorageAtom
{
// What type is stored exactly?
// Depending on that, uses one of the other fields as a storage.
var JType type;
var float numberValue;
var string stringValue;
var bool booleanValue;
// Used for storing both JSON objects and arrays.
var JSON complexValue;
// Numeric value might not fit into a `float` very well, so we will store
// them as both `float` and `integer` and allow user to request any version
// of them
var int numberValueAsInt;
var bool preferIntegerValue;
// Some `string` values might be actually used to represent classes,
// so we will give users an ability to request `string` value as a class.
var class<Object> stringValueAsClass;
// To avoid several unsuccessful attempts to load `class` object from
// a `string`, we will record whether we've already tied that.
var bool classLoadingWasAttempted;
};
/**
* Enumeration of possible result of comparing two JSON containers
* (objects or arrays).
* Containers are compared as sets of stored variables.
*/
enum JComparisonResult
{
// Containers contain different sets of values and
// neither can be considered a subset of another.
JCR_Incomparable,
// "Left" container is a subset of the "right" one.
JCR_SubSet,
// "Right" container is a subset of the "left" one.
JCR_Overset,
// Both objects are identical.
JCR_Equal
};
/**
* Describes how JSON containers are supposed to be displayed.
*/
struct JSONDisplaySettings
{
// Should it be displayed as a formatted string, with added color tags?
var bool colored;
// Should we "stack" indentation of folded objects?
var bool stackIndentation;
// Indentation for elements in object/array
var string subObjectIndentation, subArrayIndentation;
// Strings to put immediately before and after object opening: '{'
var string beforeObjectOpening, afterObjectOpening;
// Strings to put immediately before and after object closing: '}'
var string beforeObjectEnding, afterObjectEnding;
// {<beforePropertyName>"name"<afterPropertyName>:value}
var string beforePropertyName, afterPropertyName;
// {"name":<beforePropertyValue>value<afterPropertyValue>}
var string beforePropertyValue, afterPropertyValue;
// String to put immediately after comma inside object,
// can be used to break line after each property record
var string afterObjectComma;
// Strings to put immediately before and after array opening: '['
var string beforeArrayOpening, afterArrayOpening;
// Strings to put immediately before and after array closing: ']'
var string beforeArrayEnding, afterArrayEnding;
// [<beforeElement>element1<afterElement>,<afterArrayComma>...]
var string beforeElement, afterElement;
// Can be used to break line after each property record
var string afterArrayComma;
};
// Max precision that will be used when outputting JSON values as a string.
// Hardcoded to force this value between 0 and 10, inclusively.
var private const config int MAX_FLOAT_PRECISION;
/**
* Completely clears caller JSON container of all stored data.
*/
public function Clear(){}
/**
* Makes an exact copy of the caller JSON container.
*
* @return Copy of the caller JSON container object.
*/
public function JSON Clone()
{
return none;
}
/**
* Checks if caller JSON container's values form a subset of
* `rightJSON`'s values.
*
* @return `true` if caller ("left") object is a subset of `rightJSON`
* and `false` otherwise.
*/
public function bool IsSubsetOf(JSON rightJSON)
{
return false;
}
/**
* Compares caller JSON container ("left container")
* to `rightJSON` ("right container").
*
* @param rightJSON Value to compare caller object to.
* @return `JComparisonResult` describing comparison of caller `JSON` container
* to `rightJSON`.
* Always returns `false` if compared objects are of different types.
*/
public final function JComparisonResult Compare(JSON rightJSON)
{
local bool firstIsSubset, secondIsSubset;
if (rightJSON == none) return JCR_Incomparable;
firstIsSubset = IsSubsetOf(rightJSON);
secondIsSubset = rightJSON.IsSubsetOf(self);
if (firstIsSubset)
{
if (secondIsSubset) {
return JCR_Equal;
}
else {
return JCR_SubSet;
}
}
else {
if (secondIsSubset) {
return JCR_Overset;
}
else {
return JCR_Incomparable;
}
}
}
/**
* Checks if two objects are equal.
*
* A shortcut for `Compare(rightJSON) == JCR_Equal`.
*
* @param rightJSON Value to compare caller object to.
* @return `true` if caller and `rightJSON` store exactly same set of values
* (under the same names for `JObject`) and `false` otherwise.
*/
public final function bool IsEqual(JSON rightJSON)
{
return (Compare(rightJSON) == JCR_Equal);
}
/**
* Displays caller JSON container with one of the presets.
*
* Default compact preset displays JSON in as little characters as possible,
* fancy preset tries to make it human-readable with appropriate spacing and
* indentation for sub objects.
*
* See `DisplayWith()` for a more tweakable method.
*
* @param fancyPrinting Leave empty of `false` for a compact display and
* `true` to display it with a fancy preset.
* @param colorSettings Display JSON container as a formatted string,
* adding color tags to JSON syntax.
* @return String representation of caller JSON container,
* in plain format if `colorSettings == false` and
* as a formatted string if `colorSettings == true`.
*/
public final function string Display(
optional bool fancyPrinting,
optional bool colorSettings)
{
local JSONDisplaySettings settingsToUse;
// Settings are minimal by default
if (fancyPrinting) {
settingsToUse = GetFancySettings();
}
if (colorSettings) {
settingsToUse.colored = true;
}
return DisplayWith(settingsToUse);
}
/**
* Displays caller JSON container with a provided preset.
*
* See `Display()` for a simpler to use method.
*
* @param displaySettings Struct that describes precisely how to display
* caller JSON container. Can be used to emulate `Display()` call.
* @return String representation of caller JSON container in format defined by
* `displaySettings`.
*/
public function string DisplayWith(JSONDisplaySettings displaySettings)
{
return "";
}
/**
* Uses given parser to parse a new set of properties inside
* the caller JSON container.
*
* Only adds new properties if parsing the whole object was successful,
* otherwise even successfully parsed properties will be discarded.
*
* `parser` must point at the text describing a JSON object or an array
* (depending on whether a caller object is `JObject` or `JArray`) in
* a valid notation. Then it parses that container inside memory, but
* instead of creating it as a separate entity, adds it's values to
* the caller container.
* Everything that comes after parsed JSON container is discarded.
*
* This method does not try to validate passed JSON and can accept invalid
* JSON by making some assumptions, but it is an undefined behavior and
* one should not expect it.
* Method is only guaranteed to work on valid JSON.
*
* @param parser Parser that method would use to parse JSON container from
* wherever it left. It's confirmed will not be changed, but if parsing
* was successful, - it will point at the next available character.
* Do not treat `parser` being in a non-failed state as a confirmation of
* successful parsing: JSON parsing might fail regardless.
* Check return value for that.
* @return `true` if parsing was successful and `false` otherwise.
*/
public function bool ParseIntoSelfWith(Parser parser)
{
return false;
}
/**
* Parse a new set of properties inside the caller JSON container from
* a given `Text`.
*
* Only adds new properties if parsing the whole object was successful,
* otherwise even successfully parsed properties will be discarded.
*
* JSON container is parsed from a given `Text`, but instead of creating
* new object as a separate entity, method adds it's values to
* the caller container.
* Everything that comes after parsed JSON container is discarded.
*
* This method does not try to validate passed JSON and can accept invalid
* JSON by making some assumptions, but it is an undefined behavior and
* one should not expect it.
* Method is only guaranteed to work on valid JSON.
*
* @param source `Text` to get JSON container definition from.
* @return `true` if parsing was successful and `false` otherwise.
*/
public final function bool ParseIntoSelf(Text source)
{
local bool successfullyParsed;
local Parser jsonParser;
jsonParser = _.text.Parse(source);
successfullyParsed = ParseIntoSelfWith(jsonParser);
_.memory.Free(jsonParser);
return successfullyParsed;
}
/**
* Parse a new set of properties inside the caller JSON container from
* a given `string`.
*
* Only adds new properties if parsing the whole object was successful,
* otherwise even successfully parsed properties will be discarded.
*
* JSON container is parsed from a given `string`, but instead of creating
* new object as a separate entity, method adds it's values to
* the caller container.
* Everything that comes after parsed JSON container is discarded.
*
* This method does not try to validate passed JSON and can accept invalid
* JSON by making some assumptions, but it is an undefined behavior and
* one should not expect it.
* Method is only guaranteed to work on valid JSON.
*
* @param source `string` to get JSON container definition from.
* @return `true` if parsing was successful and `false` otherwise.
*/
public final function bool ParseIntoSelfString(
string source,
optional Text.StringType stringType)
{
local bool successfullyParsed;
local Parser jsonParser;
jsonParser = _.text.ParseString(source, stringType);
successfullyParsed = ParseIntoSelfWith(jsonParser);
_.memory.Free(jsonParser);
return successfullyParsed;
}
/**
* Parse a new set of properties inside the caller JSON container from
* a given raw data.
*
* Only adds new properties if parsing the whole object was successful,
* otherwise even successfully parsed properties will be discarded.
*
* JSON container is parsed from a given raw data, but instead of creating
* new object as a separate entity, method adds it's values to
* the caller container.
* Everything that comes after parsed JSON container is discarded.
*
* This method does not try to validate passed JSON and can accept invalid
* JSON by making some assumptions, but it is an undefined behavior and
* one should not expect it.
* Method is only guaranteed to work on valid JSON.
*
* @param source Raw data *array of `Text.Character`) to get JSON container
* definition from.
* @return `true` if parsing was successful and `false` otherwise.
*/
public final function bool ParseIntoSelfRaw(array<Text.Character> rawSource)
{
local bool successfullyParsed;
local Parser jsonParser;
jsonParser = _.text.ParseRaw(rawSource);
successfullyParsed = ParseIntoSelfWith(jsonParser);
_.memory.Free(jsonParser);
return successfullyParsed;
}
/**
* Checks if two `JStorageAtom` values represent the same values.
*
* Atoms storing the same value does not necessarily mean that they are equal
* as structs because they contain several different container members and
* unused ones can differ.
*
* @return `true` if atoms stores the same value, `false` otherwise.
*/
protected final function bool AreAtomsEqual(
JStorageAtom atom1,
JStorageAtom atom2)
{
if (atom1.type != atom2.type) return false;
if (atom1.type == JSON_Undefined) return true;
if (atom1.type == JSON_Null) return true;
if (atom1.type == JSON_Number) {
return ( atom1.numberValue == atom2.numberValue
&& atom1.numberValueAsInt == atom2.numberValueAsInt);
}
if (atom1.type == JSON_Boolean) {
return (atom1.booleanValue == atom2.booleanValue);
}
if (atom1.type == JSON_String) {
return (atom1.stringValue == atom2.stringValue);
}
if (atom1.complexValue == none && atom2.complexValue == none) {
return true;
}
if (atom1.complexValue == none || atom2.complexValue == none) {
return false;
}
return atom1.complexValue.IsEqual(atom2.complexValue);
}
/**
* Tries to load class variable into an atom, based on it's `stringValue`.
*
* @param atom Result will be recorded in the field of the argument itself.
*/
protected final function TryLoadingStringAsClass(out JStorageAtom atom)
{
if (atom.classLoadingWasAttempted) return;
atom.classLoadingWasAttempted = true;
atom.stringValueAsClass =
class<Object>(DynamicLoadObject(atom.stringValue, class'Class', true));
}
/**
* Displays a `JStorageAtom` in it's appropriate text JSON representation.
*
* That's a representation that can be pasted inside JSON array as-is
* (with different values separated by commas).
*
* @param atom Atom to display.
* @param displaySettings Display settings, according to which to
* display the atom.
* @return Text representation of the passed `atom`, empty if it's of
* the type `JSON_Undefined`.
*/
protected final function string DisplayAtom(
JStorageAtom atom,
JSONDisplaySettings displaySettings)
{
local string colorTag;
local string result;
if ( atom.complexValue != none
&& (atom.type == JSON_Object || atom.type == JSON_Array) ) {
return atom.complexValue.DisplayWith(displaySettings);
}
if (atom.type == JSON_Null) {
result = "null";
colorTag = "$json_null";
}
else if (atom.type == JSON_Number) {
if (atom.preferIntegerValue) {
result = string(atom.numberValueAsInt);
}
else {
result = DisplayFloat(atom.numberValue);
}
colorTag = "$json_number";
}
else if (atom.type == JSON_String) {
result = DisplayJSONString(atom.stringValue);
colorTag = "$json_string";
}
else if (atom.type == JSON_Boolean) {
if (atom.booleanValue) {
result = "true";
}
else {
result = "false";
}
colorTag = "$json_boolean";
}
if (displaySettings.colored) {
return "{" $ colorTag @ result $ "}";
}
return result;
}
// Helper function for printing float with a given max precision
// (`MAX_FLOAT_PRECISION`).
private final function string DisplayFloat(float number)
{
local int integerPart, fractionalPart;
local int precision;
local int howManyZeroes;
local string zeroes;
local string result;
precision = Clamp(MAX_FLOAT_PRECISION, 0, 10);
if (number < 0) {
number *= -1;
result = "-";
}
integerPart = number;
result $= string(integerPart);
number = (number - integerPart);
// We try to perform minimal amount of operations to extract fractional
// part as integer in order to avoid accumulating too much of an error.
fractionalPart = Round(number * (10 ** precision));
if (fractionalPart <= 0) {
return result;
}
result $= ".";
// Pad necessary zeroes in front
howManyZeroes = precision - CountDigits(fractionalPart);
while (howManyZeroes > 0) {
zeroes $= "0";
howManyZeroes -= 1;
}
// Cut off trailing zeroes and
while (fractionalPart > 0 && fractionalPart % 10 == 0) {
fractionalPart /= 10;
}
return result $ zeroes $ string(fractionalPart);
}
// Helper function that counts amount of digits in decimal representation
// of `number`.
private final function int CountDigits(int number)
{
local int digitCounter;
while (number > 0)
{
number -= (number % 10);
number /= 10;
digitCounter += 1;
}
return digitCounter;
}
/**
* Prepares a `string` to be displayed as textual JSON representation by
* replacing certain characters with their escaped sequences.
*
* @param input String value to display inside a text representation of
* a JSON data.
* @result Representation of an `input` that can be included in text form of
* JSON data.
*/
protected final function string DisplayJSONString(string input)
{
// Convert control characters (+ other, specified by JSON)
// into escaped sequences
ReplaceText(input, "\"", "\\\"");
ReplaceText(input, "/", "\\/");
ReplaceText(input, "\\", "\\\\");
ReplaceText(input, Chr(0x08), "\\b");
ReplaceText(input, Chr(0x0c), "\\f");
ReplaceText(input, Chr(0x0a), "\\n");
ReplaceText(input, Chr(0x0d), "\\r");
ReplaceText(input, Chr(0x09), "\\t");
// TODO: test if there are control characters and render them as "\u...."
return ("\"" $ input $ "\"");
}
// helper function to prepare fancy display settings, because it is a bitch to
// include a `string` with new line symbol in `defaultproperties`.
private final function JSONDisplaySettings GetFancySettings()
{
local string lineFeed;
local JSONDisplaySettings fancySettings;
lineFeed = Chr(10);
fancySettings.stackIndentation = true;
fancySettings.subObjectIndentation = " ";
fancySettings.subArrayIndentation = "";
fancySettings.afterObjectOpening = lineFeed;
fancySettings.beforeObjectEnding = lineFeed;
fancySettings.beforePropertyValue = " ";
fancySettings.afterObjectComma = lineFeed;
fancySettings.beforeElement = " ";
fancySettings.afterArrayComma = " ";
return fancySettings;
}
/**
* Helper function that prepares `JSONDisplaySettings` to be used for
* a folded object / array to make it more human-readable thanks to
* sub-object/-arrays indentation.
*
* @param inputSettings Settings to modify, passed variable will
* remain unchanged.
* @param indentingArray True if we need to modify settings for
* a folded array and `false` if for the object.
* @return Modified `inputSettings`, with added indentation.
*/
protected final function JSONDisplaySettings IndentSettings(
JSONDisplaySettings inputSettings,
optional bool indentingArray)
{
local string lineFeed;
local string lineFeedIndent;
local JSONDisplaySettings indentedSettings;
indentedSettings = inputSettings;
lineFeed = Chr(0x0a);
if (indentingArray) {
lineFeedIndent = lineFeed $ inputSettings.subArrayIndentation;
}
else {
lineFeedIndent = lineFeed $ inputSettings.subObjectIndentation;
}
if (lineFeedIndent == lineFeed) {
return indentedSettings;
}
ReplaceText(indentedSettings.afterObjectEnding, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.afterPropertyName, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.afterObjectComma, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.afterArrayOpening, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.beforeArrayEnding, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.afterArrayEnding, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.beforeElement, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.afterElement, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.afterArrayComma, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.beforeObjectOpening, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.beforePropertyValue, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.afterObjectOpening, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.beforeObjectEnding, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.beforeArrayOpening, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.afterPropertyValue, lineFeed, lineFeedIndent);
ReplaceText(indentedSettings.beforePropertyName, lineFeed, lineFeedIndent);
return indentedSettings;
}
/**
* Uses given parser to parse a single (possibly complex like JSON object
* or array) JSON value.
*
* @param parser Parser that method would use to parse JSON value from
* wherever it left. It's confirmed will not be changed, but if parsing
* was successful, - it will point at the next available character.
* Do not treat `parser` being in a non-failed state as a confirmation of
* successful parsing: JSON parsing might fail regardless.
* Check return value for that.
* @return Parsed JSON value as `JStorageAtom`.
* If parsing has failed it will have the `JSON_Undefined` type.
*/
protected final function JStorageAtom ParseAtom(Parser parser)
{
local Parser.ParserState initState;
local JStorageAtom newAtom;
if (parser == none) return newAtom;
if (!parser.Ok()) return newAtom;
initState = parser.GetCurrentState();
if (parser.MStringLiteral(newAtom.stringValue).Ok())
{
newAtom.type = JSON_String;
return newAtom;
}
newAtom = ParseLiteral(parser.RestoreState(initState));
if (newAtom.type != JSON_Undefined) {
return newAtom;
}
newAtom = ParseComplex(parser.RestoreState(initState));
if (newAtom.type != JSON_Undefined) {
return newAtom;
}
newAtom = ParseNumber(parser.RestoreState(initState));
if (newAtom.type == JSON_Undefined) {
parser.RestoreState(initState);
}
return newAtom;
}
/**
* Uses given parser to parse a "literal" JSON value:
* "true", "false" or "null".
*
* @param parser Parser that method would use to parse JSON value from
* wherever it left. It's confirmed will not be changed, but if parsing
* was successful, - it will point at the next available character.
* Do not treat `parser` being in a non-failed state as a confirmation of
* successful parsing: JSON parsing might fail regardless.
* Check return value for that.
* @return Parsed JSON value as `JStorageAtom`.
* If parsing has failed it will have the `JSON_Undefined` type.
*/
protected final function JStorageAtom ParseLiteral(Parser parser)
{
local JStorageAtom newAtom;
local Parser.ParserState initState;
initState = parser.GetCurrentState();
if (parser.Match("null", true).Ok())
{
newAtom.type = JSON_Null;
return newAtom;
}
if (parser.RestoreState(initState).Match("false", true).Ok())
{
newAtom.type = JSON_Boolean;
return newAtom;
}
if (parser.RestoreState(initState).Match("true", true).Ok())
{
newAtom.type = JSON_Boolean;
newAtom.booleanValue = true;
return newAtom;
}
}
/**
* Uses given parser to parse a complex JSON value: JSON object or array.
*
* @param parser Parser that method would use to parse JSON value from
* wherever it left. It's confirmed will not be changed, but if parsing
* was successful, - it will point at the next available character.
* Do not treat `parser` being in a non-failed state as a confirmation of
* successful parsing: JSON parsing might fail regardless.
* Check return value for that.
* @return Parsed JSON value as `JStorageAtom`.
* If parsing has failed it will have the `JSON_Undefined` type.
*/
protected final function JStorageAtom ParseComplex(Parser parser)
{
local JStorageAtom newAtom;
local Parser.ParserState initState;
initState = parser.GetCurrentState();
if (parser.Match("{").Ok())
{
newAtom.complexValue = _.json.NewObject();
newAtom.type = JSON_Object;
}
else if (parser.RestoreState(initState).Match("[").Ok())
{
newAtom.complexValue = _.json.NewArray();
newAtom.type = JSON_Array;
}
parser.RestoreState(initState);
if ( newAtom.complexValue != none
&& newAtom.complexValue.ParseIntoSelfWith(parser)) {
return newAtom;
}
newAtom.type = JSON_Undefined;
newAtom.complexValue = none;
return newAtom;
}
/**
* Uses given parser to parse a numeric JSON value.
*
* @param parser Parser that method would use to parse JSON value from
* wherever it left. It's confirmed will not be changed, but if parsing
* was successful, - it will point at the next available character.
* Do not treat `parser` being in a non-failed state as a confirmation of
* successful parsing: JSON parsing might fail regardless.
* Check return value for that.
* @return Parsed JSON value as `JStorageAtom`.
* If parsing has failed it will have the `JSON_Undefined` type.
*/
protected final function JStorageAtom ParseNumber(Parser parser)
{
local JStorageAtom newAtom;
local Parser.ParserState initState, integerParsedState;
initState = parser.GetCurrentState();
if (!parser.MInteger(newAtom.numberValueAsInt).Ok()) {
return newAtom;
}
newAtom.type = JSON_Number;
integerParsedState = parser.GetCurrentState();
// For a number check if it is recorded as a float specifically.
// If not - prefer integer for storage.
if ( parser.Match(".").Ok()
|| parser.RestoreState(integerParsedState).Match("e", true).Ok())
{
parser.RestoreState(initState).MNumber(newAtom.numberValue);
return newAtom;
}
parser.RestoreState(integerParsedState);
newAtom.numberValue = newAtom.numberValueAsInt;
newAtom.preferIntegerValue = true;
return newAtom;
}
event Destroyed()
{
super.Destroyed();
Clear();
}
defaultproperties
{
MAX_FLOAT_PRECISION = 4
}

158
sources/Data/JSON/JSONAPI.uc

@ -1,158 +0,0 @@
/**
* Provides convenient access to JSON-related functions.
* Copyright 2019 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class JSONAPI extends Singleton;
public function JObject newObject()
{
local JObject newObject;
newObject = Spawn(class'JObject');
return newObject;
}
public function JArray newArray()
{
local JArray newArray;
newArray = Spawn(class'JArray');
return newArray;
}
public function JObject ParseObjectWith(Parser jsonParser)
{
local JObject result;
result = NewObject();
if (result == none) {
return none;
}
if (!result.ParseIntoSelfWith(jsonParser))
{
result.Destroy();
return none;
}
return result;
}
public function JObject ParseObject(Text source)
{
local JObject result;
result = NewObject();
if (result == none) {
return none;
}
if (!result.ParseIntoSelf(source))
{
result.Destroy();
return none;
}
return result;
}
public function JObject ParseObjectString(string source)
{
local JObject result;
result = NewObject();
if (result == none) {
return none;
}
if (!result.ParseIntoSelfString(source))
{
result.Destroy();
return none;
}
return result;
}
public function JObject ParseObjectRaw(array<Text.Character> source)
{
local JObject result;
result = NewObject();
if (result == none) {
return none;
}
if (!result.ParseIntoSelfRaw(source))
{
result.Destroy();
return none;
}
return result;
}
public function JArray ParseArrayWith(Parser jsonParser)
{
local JArray result;
result = NewArray();
if (result == none) {
return none;
}
if (!result.ParseIntoSelfWith(jsonParser))
{
result.Destroy();
return none;
}
return result;
}
public function JArray ParseArray(Text source)
{
local JArray result;
result = NewArray();
if (result == none) {
return none;
}
if (!result.ParseIntoSelf(source))
{
result.Destroy();
return none;
}
return result;
}
public function JArray ParseArrayString(string source)
{
local JArray result;
result = NewArray();
if (result == none) {
return none;
}
if (!result.ParseIntoSelfString(source))
{
result.Destroy();
return none;
}
return result;
}
public function JArray ParseArrayRaw(array<Text.Character> source)
{
local JArray result;
result = NewArray();
if (result == none) {
return none;
}
if (!result.ParseIntoSelfRaw(source))
{
result.Destroy();
return none;
}
return result;
}
defaultproperties
{
}

1419
sources/Data/JSON/Tests/TEST_JSON.uc

File diff suppressed because it is too large Load Diff

2
sources/Feature.uc

@ -61,7 +61,7 @@ public static final function Feature EnableMe()
}
default.blockSpawning = false;
// TODO: code duplication with `Service`?
newInstance = __().Spawn(default.class);
newInstance = Feature(__().memory.Allocate(default.class));
default.blockSpawning = true;
return newInstance;
}

60
sources/Global.uc

@ -1,8 +1,8 @@
/**
* Class for an object that will provide an access to a Acedia's functionality
* by giving a reference to this actor to all Acedia's objects and actors,
* by giving a reference to this object to all Acedia's objects and actors,
* emulating a global API namespace.
* Copyright 2020 Anton Tarasenko
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -19,34 +19,50 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Global extends Singleton;
class Global extends Object;
// `Global` is expected to behave like a singleton and will store it's
// main instance in this variable's default value.
var protected Global myself;
var public RefAPI ref;
var public BoxAPI box;
var public LoggerAPI logger;
var public JSONAPI json;
var public CollectionsAPI collections;
//var public JSONAPI json;
var public AliasesAPI alias;
var public TextAPI text;
var public MemoryAPI memory;
var public ConsoleAPI console;
var public ColorAPI color;
var public UserAPI users;
var public JSONAPI json;
public final static function Global GetInstance()
{
if (default.myself == none) {
// `Global` is special and exists outside main Acedia's
// object infrastructure, so we allocate it without using API methods.
default.myself = new class'Global';
default.myself.Initialize();
}
return default.myself;
}
// TODO: APIs must be `remoteRole = ROLE_None`
protected function OnCreated()
protected function Initialize()
{
Spawn(class'LoggerAPI');
logger = LoggerAPI(class'LoggerAPI'.static.GetInstance());
Spawn(class'JSONAPI');
json = JSONAPI(class'JSONAPI'.static.GetInstance());
Spawn(class'AliasesAPI');
alias = AliasesAPI(class'AliasesAPI'.static.GetInstance());
Spawn(class'TextAPI');
text = TextAPI(class'TextAPI'.static.GetInstance());
Spawn(class'MemoryAPI');
memory = MemoryAPI(class'MemoryAPI'.static.GetInstance());
Spawn(class'ConsoleAPI');
console = ConsoleAPI(class'ConsoleAPI'.static.GetInstance());
Spawn(class'ColorAPI');
color = ColorAPI(class'ColorAPI'.static.GetInstance());
Spawn(class'UserAPI');
users = UserAPI(class'UserAPI'.static.GetInstance());
// Special case that we cannot spawn with memory API since it obviously
// does not exist yet!
memory = new class'MemoryAPI';
ref = RefAPI(memory.Allocate(class'RefAPI'));
box = BoxAPI(memory.Allocate(class'BoxAPI'));
logger = LoggerAPI(memory.Allocate(class'LoggerAPI'));
collections = CollectionsAPI(memory.Allocate(class'CollectionsAPI'));
alias = AliasesAPI(memory.Allocate(class'AliasesAPI'));
text = TextAPI(memory.Allocate(class'TextAPI'));
console = ConsoleAPI(memory.Allocate(class'ConsoleAPI'));
color = ColorAPI(memory.Allocate(class'ColorAPI'));
users = UserAPI(memory.Allocate(class'UserAPI'));
json = JSONAPI(memory.Allocate(class'JSONAPI'));
json.InitializeStatic();
}

2
sources/Logger/LoggerAPI.uc

@ -18,7 +18,7 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class LoggerAPI extends Singleton;
class LoggerAPI extends AcediaObject;
var private LoggerService logService;

32
sources/Manifest.uc

@ -1,6 +1,6 @@
/**
* Manifest is meant to describe contents of the Acedia's package.
* Copyright 2020 Anton Tarasenko
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -22,14 +22,30 @@
defaultproperties
{
features(0) = class'Commands'
commands(0) = class'ACommandHelp'
commands(1) = class'ACommandDosh'
commands(2) = class'ACommandNick'
services(0) = class'ConnectionService'
services(1) = class'PlayerService'
aliasSources(0) = class'AliasSource'
aliasSources(1) = class'WeaponAliasSource'
aliasSources(2) = class'ColorAliasSource'
testCases(0) = class'TEST_Aliases'
testCases(1) = class'TEST_ColorAPI'
testCases(2) = class'TEST_JSON'
testCases(3) = class'TEST_Text'
testCases(4) = class'TEST_TextAPI'
testCases(5) = class'TEST_Parser'
testCases(6) = class'TEST_User'
testCases(0) = class'TEST_Base'
testCases(1) = class'TEST_Boxes'
testCases(2) = class'TEST_Refs'
testCases(3) = class'TEST_Aliases'
testCases(4) = class'TEST_ColorAPI'
testCases(5) = class'TEST_Text'
testCases(6) = class'TEST_TextAPI'
testCases(7) = class'TEST_Parser'
testCases(8) = class'TEST_JSON'
testCases(9) = class'TEST_TextCache'
testCases(10) = class'TEST_User'
testCases(11) = class'TEST_Memory'
testCases(12) = class'TEST_DynamicArray'
testCases(13) = class'TEST_AssociativeArray'
testCases(14) = class'TEST_Iterator'
testCases(15) = class'TEST_Command'
testCases(16) = class'TEST_CommandDataBuilder'
}

349
sources/Memory/MemoryAPI.uc

@ -1,11 +1,9 @@
/**
* API that provides functions for managing objects and actors by providing
* easy and general means to create and destroy them, that allow to make use of
* temporary `Object`s in a more efficient way.
* This is a low-level API that most users of Acedia, most likely,
* would not have to use, since creation of most objects would use their own
* wrapper functions around this API.
* Copyright 2020 Anton Tarasenko
* API that provides methods for managing objects and actors by providing
* simple and general means to create and destroy them in a way that is managed
* by Acedia and allows to use object pools, constructors and finalizers.
* These methods should be used for all Acedia's objects and actors.
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -22,42 +20,23 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MemoryAPI extends Singleton;
class MemoryAPI extends AcediaObject;
// This variable counts ticks and should be different each new tick.
var private int currentTick;
// Stores instance of an `Object` that can be borrowed from the pool.
struct BorrowableRecord
{
// Borrowable instance
var Object instance;
// Was this object borrowed?
// This flag will persist unless object was explicitly freed,
// even if borrowed reference timed out.
var bool borrowed;
// When was this object borrowed?
// Used to automatically free borrowed objects after the tick has passed.
var int borrowTick;
};
// Available object pools
var private array<BorrowableRecord> borrowPool;
// Checks if instance in the given `record` is borrowed.
private final function bool IsBorrowed(BorrowableRecord record)
/**
* Creates a class instance from it's string representation.
*
* Does not generate log messages upon failure.
*
* @param classReference String representation of the class to return.
* @return Loaded class, corresponding to it's name from `classReference`.
*/
public final function class<Object> LoadClass(Text classReference)
{
// `record.borrowed` means instance was borrowed,
// but not explicitly freed;
// `record.borrowTick >= currentTick` means that rights to the borrowed
// instance hasn't yet ran out.
return (record.borrowed && record.borrowTick >= currentTick);
if (classReference == none) {
return none;
}
// Loads a reference to class instance from it's string representation.
private final function class<Object> LoadClass(string classReference)
{
return class<Object>(DynamicLoadObject(classReference, class'Class', true));
return class<Object>( DynamicLoadObject(classReference.ToPlainString(),
class'Class', true));
}
/**
@ -66,225 +45,189 @@ private final function class<Object> LoadClass(string classReference)
* If uses a proper spawning mechanism for both objects (`new`)
* and actors (`Spawn`).
*
* For Acedia's objects / actors makes use of their object pools and
* calls constructors.
*
* If Acedia's object / actor does make use of object pools, -
* guarantees to return last pooled object (in a LIFO queue),
* unless `forceNewInstance == true`.
*
* @param classToAllocate Class of the `Object` / `Actor` that this method
* must create.
* @return Newly created object, might be `none` if creation has failed.
* @param forceNewInstance Set this to `true` if you require this method to
* create a new instance, bypassing any object pools.
* @return Newly created object,
* `none` if creation has failed (only possible for actors).
*/
public final function Object Allocate(class<Object> classToAllocate)
{
local class<Actor> actorClassToSpawn;
public final function Object Allocate(
class<Object> classToAllocate,
optional bool forceNewInstance)
{
local Object allocatedObject;
local AcediaObjectPool relevantPool;
local class<AcediaObject> acediaObjectClassToAllocate;
local class<AcediaActor> acediaActorClassToAllocate;
local class<Actor> actorClassToAllocate;
if (classToAllocate == none) return none;
actorClassToSpawn = class<Actor>(classToAllocate);
if (actorClassToSpawn != none)
// Try using pool first (only if new instance is not required)
acediaObjectClassToAllocate = class<AcediaObject>(classToAllocate);
acediaActorClassToAllocate = class<AcediaActor>(classToAllocate);
if (!forceNewInstance)
{
return Spawn(actorClassToSpawn);
if (acediaObjectClassToAllocate != none) {
relevantPool = acediaObjectClassToAllocate.static._getPool();
}
return (new classToAllocate);
if (acediaActorClassToAllocate != none) {
relevantPool = acediaActorClassToAllocate.static._getPool();
}
/**
* Creates a new `Object` / `Actor` of a given class.
*
* If uses a proper spawning mechanism for both objects (`new`)
* and actors (`Spawn`).
*
* @param classToAllocate Text representation (name) of the class of the
* `Object` / `Actor` that this method must create.
* Should contain full package-path.
* @return Newly created object, might be `none` if creation has failed.
*/
public final function Object AllocateByReference(string refToClassToAllocate)
{
return Allocate(LoadClass(refToClassToAllocate));
// `relevantPool == none` is expected if object / actor of is setup to
// not use object pools.
if (relevantPool != none) {
allocatedObject = relevantPool.Fetch();
}
/**
* Borrows an instance of an `Object` / `Actor` of the given class
* from the pool.
* Borrowed instance will be auto-freed during next tick.
*
* @param classToBorrow Class of an `Object` / `Actor` we want to borrow.
* @return Borrowed object, might be `none` if borrow pool is empty and
* creation of a new `Object` / `Actor` has failed.
*/
public final function Object Borrow(class<Object> classToBorrow)
}
// If pools did not work - spawn / create object through regular methods
if (allocatedObject == none)
{
local int i;
local BorrowableRecord newRecord;
for (i = 0; i < borrowPool.length; i += 1)
actorClassToAllocate = class<AcediaActor>(classToAllocate);
if (actorClassToAllocate != none)
{
if (IsBorrowed(borrowPool[i])) continue;
if (borrowPool[i].instance == none) continue;
if (borrowPool[i].instance.class != classToBorrow) continue;
borrowPool[i].borrowed = true;
borrowPool[i].borrowTick = currentTick;
return borrowPool[i].instance;
allocatedObject = class'CoreService'.static
.GetInstance()
.Spawn(actorClassToAllocate);
}
// Create a new instance to borrow, if there isn't any available for
// the given class.
newRecord.borrowed = false;
newRecord.instance = Allocate(classToBorrow);
if (newRecord.instance != none)
{
borrowPool[borrowPool.length] = newRecord;
else {
allocatedObject = (new classToAllocate);
}
return newRecord.instance;
}
/**
* Borrows an instance of an `Object` / `Actor` of the given class
* from the pool.
* Borrowed instance will be auto-freed during next tick.
*
* @param classToBorrow Text representation (name) of the class of
* an `Object` / `Actor` we want to borrow.
* @return Borrowed object, might be `none` if borrow pool is empty and
* creation of a new `Object` / `Actor` has failed.
*/
public final function Object BorrowByReference(string refToClassToBorrow)
{
return Borrow(LoadClass(refToClassToBorrow));
// Call constructors (also do it for actors, just in case)
if (acediaObjectClassToAllocate != none) {
AcediaObject(allocatedObject)._constructor();
}
/**
* Claims an instance of an `Object` / `Actor` of the given class
* from the pool.
* Claimed instances are removed from the borrow pool and
* will not be automatically freed.
*
* @param classToClaim Class of an `Object` / `Actor` we wish to borrow.
* @return Borrowed object, might be `none` if borrow pool is empty and
* creation of a new `Object` / `Actor` has failed.
*/
public final function Object Claim(class<Object> classToClaim)
{
local int i;
local Object instance;
for (i = 0; i < borrowPool.length; i += 1)
{
if (IsBorrowed(borrowPool[i])) continue;
if (borrowPool[i].instance == none) continue;
if (borrowPool[i].instance.class != classToClaim) continue;
instance = borrowPool[i].instance;
borrowPool.Remove(i, 1);
return instance;
if (acediaActorClassToAllocate != none) {
AcediaActor(allocatedObject)._constructor();
}
// Create a new instance to borrow, if there isn't any available for
// the given class.
return Allocate(classToClaim);
return allocatedObject;
}
/**
* Claims an instance of an `Object` / `Actor` of the given class
* from the pool.
* Claimed instances are removed from the borrow pool and
* will not be automatically freed.
* Creates a new `Object` / `Actor` of a class, given by it's
* string representation.
*
* If uses a proper spawning mechanism for both objects (`new`)
* and actors (`Spawn`).
*
* For Acedia's objects / actors makes use of their object pools and
* calls constructors.
*
* If Acedia's object / actor does make use of object pools, -
* guarantees to return last pooled object (in a LIFO queue),
* unless `forceNewInstance == true`.
*
* @param classToClaim Text representation (name) of the class of
* an `Object` / `Actor` we wish to claim.
* @return Borrowed object, might be `none` if borrow pool is empty and
* creation of a new `Object` / `Actor` has failed.
* @param classToAllocate Class of the `Object` / `Actor` that this method
* must create.
* @param forceNewInstance Set this to `true` if you require this method to
* create a new instance, bypassing any object pools.
* @return Newly created object,
* `none` if creation has failed (only possible for actors).
*/
public final function Object ClaimByReference(string refToClassToClaim)
public final function Object AllocateByReference(
Text refToClassToAllocate,
optional bool forceNewInstance)
{
return Claim(LoadClass(refToClassToClaim));
return Allocate(LoadClass(refToClassToAllocate), forceNewInstance);
}
/**
* Frees given `Object` / `Actor` resource.
*
* By default `Actor`s are destroyed.
* Due to limitations of the engine objects cannot be outright destroyed.
* Instead, they are put into a "borrow pool", from where they can later be
* taken for a reuse.
* If Acedia's object or actor is passed, method will try to store it
* in an object pool.
*
* @param objectToDelete `Object` / `Actor` that must be freed.
* @param forceMakeBorrowable Only has an effect if `objectToDelete`
* is an `Actor`, in which case it forces it to be added
* to the borrow pool, instead of being destroyed.
*/
public final function Free
(
Object objectToDelete,
optional bool forceMakeBorrowable
)
public final function Free(Object objectToDelete)
{
local int i;
local Actor actorToDelete;
local BorrowableRecord newRecord;
local AcediaObjectPool relevantPool;
local Actor objectAsActor;
local AcediaActor objectAsAcediaActor;
local AcediaObject objectAsAcediaObject;
if (objectToDelete == none) return;
actorToDelete = Actor(objectToDelete);
if (actorToDelete != none && !forceMakeBorrowable)
// Call finalizers for Acedia's objects and actors
objectAsAcediaObject = AcediaObject(objectToDelete);
objectAsAcediaActor = AcediaActor(objectToDelete);
if (objectAsAcediaObject != none)
{
actorToDelete.Destroy();
if (!objectAsAcediaObject.IsAllocated()) {
return;
}
// Check if `objectToDelete` is already in our records.
for (i = 0; i < borrowPool.length; i += 1)
{
if (borrowPool[i].instance == objectToDelete)
relevantPool = objectAsAcediaObject._getPool();
objectAsAcediaObject._finalizer();
}
if (objectAsAcediaActor != none)
{
borrowPool[i].borrowed = false;
if (!objectAsAcediaActor.IsAllocated()) {
return;
}
relevantPool = objectAsAcediaActor._getPool();
objectAsAcediaActor._finalizer();
}
// Try to store freed object in a pool
if (relevantPool != none && relevantPool.Store(objectToDelete)) {
return;
}
// Otherwise destroy actors and forget about objects
objectAsActor = Actor(objectToDelete);
if (objectAsActor != none) {
objectAsActor.Destroy();
}
}
/**
* Frees given array of `Object` / `Actor` resources.
*
* If Acedia's object or actor is contained in the passed array,
* method will try to store it in an object pool.
*
* @param objectsToDelete `Object` / `Actor` that must be freed.
*/
public final function FreeMany(array<Object> objectsToDelete)
{
local int i;
for (i = 0; i < objectsToDelete.length; i += 1) {
Free(objectsToDelete[i]);
}
// If not - add it
newRecord.instance = objectToDelete;
newRecord.borrowed = false;
borrowPool[borrowPool.length] = newRecord;
}
/**
* Forces Unreal Engine to do garbage collection.
* By default also cleans up all the objects in the borrow object pool.
* By default also cleans up all the objects pools registered in
* `MemoryService`, which includes all of the pools for
* Acedia's built-in classes.
*
* Process of garbage collection causes significant lag spike during the game
* and should be used carefully.
*
* NOTE: method does not guarantee that borrow pool will be empty after
* this call (even with `keepBorrowedObjectPool = true`),
* since some of the borrowable objects might be currently in use and,
* therefore, cannot be garbage collected.
* and should be used sparingly and at right moments..
*
* @param keepBorrowedObjectPool Set this to `true` to NOT garbage collect
* @param keepAcediaPools Set this to `true` to NOT garbage collect
* objects in a borrow pool. Otherwise keep it `false`.
*/
public final function CollectGarbage(optional bool keepBorrowedObjectPool)
public final function CollectGarbage(optional bool keepAcediaPools)
{
local int i;
if (!keepBorrowedObjectPool)
{
// Dereference all non-borrowed objects from borrow pool,
// so that they can be garbage collected.
i = 0;
while (i < borrowPool.length)
{
if ( borrowPool[i].instance == none
|| !IsBorrowed(borrowPool[i]) )
{
borrowPool.Remove(i, 1);
}
else
local MemoryService service;
// Drop content of all `AcediaObjectPools` first
if (!keepAcediaPools)
{
i += 1;
}
service = MemoryService(class'MemoryService'.static.Require());
if (service != none) {
service.ClearAll();
}
}
// This makes Unreal Engine do garbage collection
ConsoleCommand("obj garbage");
}
event Tick(float delta)
{
currentTick += 1;
class'CoreService'.static.GetInstance().ConsoleCommand("obj garbage");
}
// TODO: add cleaning on cooldown
defaultproperties
{
currentTick = 0
}

67
sources/Memory/MemoryService.uc

@ -0,0 +1,67 @@
/**
* This service is meant to perform auxiliary functions for `MemoryAPI`.
* It's main task is to keep track of all `AcediaObjectPool`s to force them to
* get rid of object references before garbage collection.
* Copyright 2019 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MemoryService extends Service;
var private array<AcediaObjectPool> registeredPools;
/**
* Registers new object pool to auto-clean upon Acedia's garbage collection.
*
* Registered `AcediaObjectPool`s will persist even if `MemoryService` is
* destroyed and re-created.
*
* @param newPool Pool that service must clean upon a `ClearAll()` call.
* @return `true` if `newPool` was registered,
* `false` if `newPool == none` or was already registered.
*/
public final function bool RegisterNewPool(AcediaObjectPool newPool)
{
local int i;
if (newPool == none) {
return false;
}
registeredPools = default.registeredPools;
for (i = 0; i < registeredPools.length; i += 1) {
if (registeredPools[i] == newPool) return false;
}
registeredPools[registeredPools.length] = newPool;
default.registeredPools = registeredPools;
return true;
}
/**
* Clears all registered (via `RegisterNewPool()`) pools.
*/
public final function ClearAll()
{
local int i;
registeredPools = default.registeredPools;
for (i = 0; i < registeredPools.length; i += 1)
{
if (registeredPools[i] == none) continue;
registeredPools[i].Clear();
}
}
defaultproperties
{
}

38
sources/Memory/Tests/MockActor.uc

@ -0,0 +1,38 @@
/**
* Mock actor class for testing `MemoryAPI` and
* it's actor allocation/deallocation.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MockActor extends AcediaActor;
var public int actorCount;
protected function Constructor()
{
default.actorCount += 1;
}
protected function Finalizer()
{
default.actorCount -= 1;
}
defaultproperties
{
actorCount = 0
}

26
sources/Memory/Tests/MockActorWithPool.uc

@ -0,0 +1,26 @@
/**
* Mock actor class for testing `MemoryAPI` and
* it's actor allocation/deallocation.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MockActorWithPool extends AcediaActor;
defaultproperties
{
usesObjectPool = true
}

30
sources/AcediaActor.uc → sources/Memory/Tests/MockObject.uc

@ -1,8 +1,6 @@
/**
* Actor base class to be used to Acedia instead of an `Actor`.
* The only difference is defined `_` member that provides convenient access to
* Acedia's API.
* It isn't guaranteed that `default._` will be defined for `AcediaActor`s.
* Mock object class for testing `MemoryAPI` and
* it's object allocation/deallocation.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
@ -20,31 +18,21 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AcediaActor extends Actor
abstract;
class MockObject extends AcediaObject;
var protected Global _;
var public int objectCount;
public final function Text T(string string)
protected function Constructor()
{
return _.text.FromString(string);
default.objectCount += 1;
}
public static final function Global __()
protected function Finalizer()
{
return Global(class'Global'.static.GetInstance());
}
event PreBeginPlay()
{
super.PreBeginPlay();
if (_ == none)
{
_ = Global(class'Global'.static.GetInstance());
default._ = _;
}
default.objectCount -= 1;
}
defaultproperties
{
objectCount = 0
}

26
sources/Memory/Tests/MockObjectNoPool.uc

@ -0,0 +1,26 @@
/**
* Mock object class for testing `MemoryAPI` and
* it's object allocation/deallocation.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MockObjectNoPool extends AcediaObject;
defaultproperties
{
usesObjectPool = false
}

245
sources/Memory/Tests/TEST_Memory.uc

@ -0,0 +1,245 @@
/**
* Set of tests related to `MemoryAPI` class and the chain of events related to
* creating/destroying Acedia's objects / actors.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_Memory extends TestCase
abstract;
protected static function TESTS()
{
Test_ObjectConstructorsFinalizers();
Test_ActorConstructorsFinalizers();
Test_ObjectPoolUsage();
Test_ActorPoolUsage();
Test_LifeVersionIsUnique();
}
protected static function Test_LifeVersionIsUnique()
{
local int i, j;
local int nextVersion;
local MockObject obj;
local MockActorWithPool act;
local array<int> objectVersions;
local array<int> actorVersions;
local bool versionsRepeated;
// Deallocate and reallocate same object/actor a bunch of times and
// ensure that every single time a unique number is returned.
// Not a comprehensive test of uniqueness, but such is impossible.
for (i = 0; i < 1000 && !versionsRepeated; i += 1)
{
// Object
obj = MockObject(__().memory.Allocate(class'MockObject'));
nextVersion = obj.GetLifeVersion();
for (j = 0; j < objectVersions.length; j += 1)
{
if (nextVersion == objectVersions[j])
{
versionsRepeated = true;
break;
}
}
objectVersions[objectVersions.length] = nextVersion;
__().memory.Free(obj);
// Actor
act = MockActorWithPool(__().memory.Allocate(class'MockActorWithPool'));
nextVersion = act.GetLifeVersion();
for (j = 0; j < actorVersions.length; j += 1)
{
if (nextVersion == actorVersions[j])
{
versionsRepeated = true;
break;
}
}
actorVersions[actorVersions.length] = nextVersion;
__().memory.Free(act);
}
Context("Testing that `GetLifeVersion()` returns unique value for Acedia's"
@ "actors/objects after each reallocation.");
Issue("`GetLifeVersion()` repeats the same version within 1000 attempts.");
TEST_ExpectFalse(versionsRepeated);
}
protected static function Test_ObjectConstructorsFinalizers()
{
local MockObject obj1, obj2;
Context("Testing that Acedia object's constructors and finalizers are"
@ "called properly.");
Issue("Object's constructor is not called.");
obj1 = MockObject(__().memory.Allocate(class'MockObject'));
TEST_ExpectTrue(class'MockObject'.default.objectCount == 1);
obj2 = MockObject(__().memory.Allocate(class'MockObject'));
TEST_ExpectTrue(class'MockObject'.default.objectCount == 2);
Issue("Object's finalizer is not called.");
__().memory.Free(obj1);
TEST_ExpectTrue(class'MockObject'.default.objectCount == 1);
Issue("`IsAllocated()` returns `false` for allocated objects.");
TEST_ExpectTrue(obj2.IsAllocated());
Issue("`IsAllocated()` returns `true` for deallocated objects.");
TEST_ExpectFalse(obj1.IsAllocated());
Issue("Object's finalizer is called for already freed object.");
__().memory.Free(obj1);
TEST_ExpectTrue(class'MockObject'.default.objectCount == 1);
Issue("Object's finalizer is not called.");
__().memory.Free(obj2);
TEST_ExpectTrue(class'MockObject'.default.objectCount == 0);
}
protected static function Test_ActorConstructorsFinalizers()
{
local MockActor act1, act2;
local MockActorWithPool poolActor;
Context("Testing that Acedia actor's constructors and finalizers are"
@ "called properly.");
Issue("Actor's constructor is not called.");
act1 = MockActor(__().memory.Allocate(class'MockActor'));
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
act2 = MockActor(__().memory.Allocate(class'MockActor'));
TEST_ExpectTrue(class'MockActor'.default.actorCount == 2);
Issue("Actor's finalizer is not called.");
__().memory.Free(act1);
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
Issue("`IsAllocated()` returns `false` for allocated actors.");
TEST_ExpectTrue(act2.IsAllocated());
Issue("`IsAllocated()` returns `true` for deallocated actors.");
poolActor =
MockActorWithPool(__().memory.Allocate(class'MockActorWithPool'));
__().memory.Free(poolActor);
TEST_ExpectNotNone(poolActor);
TEST_ExpectFalse(poolActor.IsAllocated());
Issue("Actor's finalizer is called for already freed object.");
__().memory.Free(act1);
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
Issue("Actor's finalizer is not called.");
__().memory.Free(act2);
TEST_ExpectTrue(class'MockActor'.default.actorCount == 0);
}
protected static function Test_ObjectPoolUsage()
{
local bool allocatedNewObject;
local int i, j;
local MockObject temp;
local array<MockObject> objects;
local MockObjectNoPool obj1, obj2;
Context("Testing usage of object pools by `MockObject`s.");
Issue("Object pool is not utilized enough.");
for (i = 0; i < 200; i += 1)
{
objects[objects.length] =
MockObject(__().memory.Allocate(class'MockObject'));
}
for (i = 0; i < 200; i += 1) {
__().memory.Free(objects[i]);
}
for (i = 0; i < 200; i += 1)
{
temp = MockObject(__().memory.Allocate(class'MockObject'));
// Have to find just allocated object among already free ones
j = 0;
allocatedNewObject = true;
while (j < objects.length)
{
if (objects[j] == temp)
{
allocatedNewObject = false;
objects.Remove(j, 1);
break;
}
j += 1;
}
if (allocatedNewObject) {
break;
}
}
TEST_ExpectFalse(allocatedNewObject);
Issue("Disabling pool for a class does not prevent pooling objects.");
obj1 = MockObjectNoPool(__().memory.Allocate(class'MockObjectNoPool'));
__().memory.Free(obj1);
obj2 = MockObjectNoPool(__().memory.Allocate(class'MockObjectNoPool'));
TEST_ExpectTrue(obj1 != obj2);
}
/*Test case [Memory] AllocationDeallocation: failed!
Testing usage of object pools by `MockActor`s.
Object pool is not utilized enough. [1]
Testing that `GetLifeVersion()` returns unique value for Acedia's actors/objects after each reallocation.
`GetLifeVersion()` repeats the same version within 1000 attempts. [1]*/
protected static function Test_ActorPoolUsage()
{
local bool allocatedNewActor;
local int i, j;
local MockActorWithPool temp;
local array<MockActorWithPool> actors;
local MockActor noPoolActor;
Context("Testing usage of object pools by `MockActor`s.");
Issue("Object pool is not utilized enough.");
for (i = 0; i < 200; i += 1)
{
actors[actors.length] =
MockActorWithPool(__().memory.Allocate(class'MockActorWithPool'));
}
for (i = 0; i < 200; i += 1) {
__().memory.Free(actors[i]);
}
for (i = 0; i < 200; i += 1)
{
temp =
MockActorWithPool(__().memory.Allocate(class'MockActorWithPool'));
// Have to find just allocated object among already free ones
j = 0;
allocatedNewActor = true;
while (j < actors.length)
{
if (actors[j] == temp)
{
allocatedNewActor = false;
actors.Remove(j, 1);
break;
}
j += 1;
}
if (allocatedNewActor) {
break;
}
}
TEST_ExpectFalse(allocatedNewActor);
Issue("Disabling pool for a class does not prevent pooling actors.");
noPoolActor = MockActor(__().memory.Allocate(class'MockActor'));
__().memory.Free(noPoolActor);
TEST_ExpectNone(noPoolActor);
}
defaultproperties
{
caseGroup = "Memory"
caseName = "AllocationDeallocation"
}

292
sources/Players/APlayer.uc

@ -1,12 +1,12 @@
/**
* Represents a connected player connection and serves to provide access to
* both it's server data and in-game pawn representation.
* Unlike `User`, - changes when player reconnects the server.
* Unlike `User`, - changes when player reconnects to the server.
* This object SHOULD NOT be created manually, please rely on
* `AcediaCore` for that.
* Killing floor 1 note: inherently linked to
* a particular `PlayerController`.
* Copyright 2020 Anton Tarasenko
* Acedia for that.
* Due to being relatively rarely created, does not use object pools,
* which simplifies their usage and comparison.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -23,16 +23,73 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class APlayer extends AcediaActor;
class APlayer extends AcediaObject;
// How this `APlayer` is identified by the server
var private User identity;
// Controller
var private PlayerController ownerController;
// Shortcut to `ConnectionEvents`, so that we don't have to write
// `class'ConnectionEvents'` every time.
var const class<PlayerEvents> events;
// Writer that can be used to write into this player's console
var private ConsoleWriter consoleInstance;
// Remember version to reallocate writer in case someone deallocates it
var private int consoleLifeVersion;
// These variables record name of this player;
// `hashedName` is used to track outside changes that bypass our getter/setter.
var private Text textName;
var private string hashedName;
// Describes the player's admin status (as defined by standard KF classes)
enum AdminStatus
{
// Not an admin
AS_None,
// (Publicly visible) admin
AS_Admin,
// Admin with their admin status hidden
AS_SilentAdmin
};
// `PlayerController` associated with the caller `APLayer`.
// Can return `none` if:
// 1. Caller `APlayer` has already disconnected;
// 2. It was not properly initialized;
// 3. There is an issue running `PlayerService`.
private final function PlayerController GetController()
{
local PlayerService service;
service = PlayerService(class'PlayerService'.static.Require());
if (service != none) {
return service.GetController(self);
}
return none;
}
// `PlayerReplicationInfo` associated with the caller `APLayer`.
// Can return `none` if:
// 1. Caller `APlayer` has already disconnected;
// 2. It was not properly initialized;
// 3. There is an issue running `PlayerService`.
private final function PlayerReplicationInfo GetRI()
{
local PlayerController myController;
myController = GetController();
if (myController != none) {
return myController.playerReplicationInfo;
}
return none;
}
/**
* Checks if player, corresponding to `APlayer`, is still connected to
* the server. If player is disconnected - `APlayer` instance should be
* considered useless.
*
* @return `true` if player is connected and `false` otherwise.
*/
public final function bool IsConnected()
{
return (GetController() != none);
}
/**
* Initializes caller `APlayer`. Should be called right after `APlayer`
@ -44,47 +101,216 @@ var const class<PlayerEvents> events;
*
* @param newController Controller that caller `APLayer` will correspond to.
*/
public final function Initialize(PlayerController newController)
public final function Initialize(Text idHash)
{
local PlayerService service;
local PlayerController myController;
local PlayerReplicationInfo myReplicationInfo;
identity = _.users.FetchByIDHash(idHash);
// Retrieve controller and replication info
service = PlayerService(class'PlayerService'.static.Require());
myController = service.GetController(self);
if (myController != none) {
myReplicationInfo = myController.playerReplicationInfo;
}
// Hash current name
if (myReplicationInfo != none) {
hashedName = myReplicationInfo.playerName;
textName = _.text.FromColoredString(hashedName);
}
}
/**
* Returns `User` object that is corresponding to the caller `APlayer`.
*
* @return `User` corresponding to the caller `APlayer`. Guarantee to be
* not `none` for correctly initialized `APlayer` (it remembers `User`
* record even if player has disconnected).
*/
public final function User GetIdentity()
{
return identity;
}
/**
* Returns current displayed name of the caller player.
*
* @return `Text` containing current name of the caller player.
* Guaranteed to not be `none`. Returned object is not managed by caller
* `APlayer` and should be manually deallocated.
*/
public final function Text GetName()
{
local PlayerReplicationInfo myReplicationInfo;
myReplicationInfo = GetRI();
if (myReplicationInfo == none) {
return P("").Copy();
}
if (textName != none && myReplicationInfo.playerName == hashedName) {
return textName.Copy();
}
_.memory.Free(textName);
hashedName = myReplicationInfo.playerName;
textName = _.text.FromColoredString(hashedName);
return textName.Copy();
}
/**
* Set new displayed name for the caller `APlayer`.
*
* @param newPlayerName New name of the caller `APlayer`. This value will
* be copied. Passing `none` will result in an empty name.
*/
public final function SetName(Text newPlayerName)
{
local PlayerReplicationInfo myReplicationInfo;
myReplicationInfo = GetRI();
if (myReplicationInfo == none) return;
_.memory.Free(textName);
// Filter both `none` and empty `newPlayerName`, so that we can
// later rely on it having at least one character
if (newPlayerName == none || newPlayerName.IsEmpty()) {
textName = P("").Copy();
}
else {
textName = newPlayerName.Copy();
}
hashedName = textName.ToColoredString(,, _.color.White);
// To correctly display nicknames we want to drop default color tag
// at the beginning (the one `ToColoredString()` adds if first character
// has no defined color).
// This is a compatibility consideration with vanilla UIs that use
// color codes from `myReplicationInfo.playerName` for displaying nicknames
// and whos expected behavior can get broken by default color tag.
if (!newPlayerName.GetFormatting(0).isColored) {
hashedName = Mid(hashedName, 4);
}
myReplicationInfo.playerName = hashedName;
}
/**
* Returns admin status of the caller player.
* Disconnected players are never admins.
*
* Different from `IsAdmin()` since this method allows to distinguish between
* different types of admin login (like silent admins).
*
* @return Admin status of the caller `APLayer`.
*/
public final function AdminStatus GetAdminStatus()
{
local PlayerReplicationInfo myReplicationInfo;
myReplicationInfo = GetRI();
if (myReplicationInfo == none) {
return AS_None;
}
if (myReplicationInfo.bAdmin) {
return AS_Admin;
}
if (myReplicationInfo.bSilentAdmin) {
return AS_SilentAdmin;
}
return AS_None;
}
/**
* Checks if caller player has admin rights.
* Disconnected players never have admin rights.
*
* Different from `GetAdminStatus()` since this method simply checks admin
* rights, without distinguishing between different types of admin login
* (like silent admins).
*
* @return `true` if player has admin rights and `false` otherwise.
*/
public final function bool IsAdmin()
{
ownerController = newController;
identity = _.users.FetchByIDHash(newController.GetPlayerIDHash());
events.static.CallPlayerConnected(self);
return (GetAdminStatus() != AS_None);
}
/**
* Returns associated controller.
* Changes admin status of the caller `APlayer`.
* Can only fail if caller `APlayer` has already disconnected.
*
* @return Controller that caller `APLayer` corresponds to.
* @param newAdminStatus New admin status of the `APlayer`.
*/
public final function PlayerController GetController()
public final function SetAdminStatus(AdminStatus newAdminStatus)
{
local PlayerReplicationInfo myReplicationInfo;
myReplicationInfo = GetRI();
if (myReplicationInfo == none) {
return;
}
switch (newAdminStatus)
{
return ownerController;
case AS_Admin:
myReplicationInfo.bAdmin = true;
myReplicationInfo.bSilentAdmin = false;
break;
case AS_SilentAdmin:
myReplicationInfo.bAdmin = false;
myReplicationInfo.bSilentAdmin = true;
break;
default:
myReplicationInfo.bAdmin = false;
myReplicationInfo.bSilentAdmin = false;
}
}
/**
* IMPORTANT: this is a helper function that is not supposed to be
* called manually.
* Returns current amount of money caller `APlayer` has.
*
* Causes `APlayer` to update it's inner state and should be triggered by
* various outside events. A necessary work-around, since we cannot make
* an event to trigger a protected function.
* @return Amount of money `APlayer` has. If player has already disconnected
* method will return `0`.
*/
public final function Update()
public final function int GetDosh()
{
if (ownerController == none) {
events.static.CallPlayerDisconnected(self);
Destroy();
local PlayerReplicationInfo myReplicationInfo;
myReplicationInfo = GetRI();
if (myReplicationInfo == none) {
return 0;
}
return myReplicationInfo.score;
}
// This is one of the most important objects for `Acedia` and should be kept
// up-to-date as much as possible.
event Tick(float delta)
/**
* Sets amount of money that caller `APlayer` will have.
*
* @param newDoshAmount New amount of money that caller `APlayer` must have.
*/
public final function SetDosh(int newDoshAmount)
{
Update();
local PlayerReplicationInfo myReplicationInfo;
myReplicationInfo = GetRI();
if (myReplicationInfo == none) {
return;
}
myReplicationInfo.score = newDoshAmount;
}
/**
* Return `ConsoleWriter` that can be used to write into this player's console.
*
* Provided that returned object is never deallocated - returns the same object
* with each call, otherwise can allocate new instance of `ConsoleWriter`.
*
* @return `ConsoleWriter` that can be used to write into this player's
* console. Returned object should not be deallocated, but it is
* guaranteed to be valid for non-disconnected players.
*/
public final function ConsoleWriter Console()
{
if ( consoleInstance == none
|| consoleInstance.GetLifeVersion() != consoleLifeVersion)
{
consoleInstance = _.console.For(GetController());
consoleLifeVersion = consoleInstance.GetLifeVersion();
}
return consoleInstance;
}
defaultproperties
{
events = class'PlayerEvents'
usesObjectPool = false
}

4
sources/Players/ConnectionListener_Player.uc

@ -24,7 +24,7 @@ static function ConnectionEstablished(ConnectionService.Connection connection)
local PlayerService service;
service = PlayerService(class'PlayerService'.static.Require());
if (service == none) {
_().logger.Fatal("Cannot start `PlayerService` service"
__().logger.Fatal("Cannot start `PlayerService` service"
@ "Acedia will not properly work from now on.");
return;
}
@ -36,7 +36,7 @@ static function ConnectionLost(ConnectionService.Connection connection)
local PlayerService service;
service = PlayerService(class'PlayerService'.static.Require());
if (service == none) {
_().logger.Fatal("Cannot start `PlayerService` service"
__().logger.Fatal("Cannot start `PlayerService` service"
@ "Acedia will not properly work from now on.");
return;
}

131
sources/Players/PlayerService.uc

@ -1,6 +1,9 @@
/**
* Service for tracking currently connected players.
* Copyright 2020 Anton Tarasenko
* Service for tracking currently connected players and remembering what
* `APlayer` is connected to what `PlayerController` (`PlayerController`
* instance is an `Actor` and therefore should not be stores as `APlayer`'s
* variable, since `APlayer` is not an `Actor`).
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -19,24 +22,18 @@
*/
class PlayerService extends Service;
// Record of all current players
var private array<APlayer> allPlayers;
// Cleans all our player records just in case something caused certain
// `APlayer` to get destroyed.
private final function RemoveNonePlayers()
{
local int i;
while (i < allPlayers.length)
// Used to 1-to-1 associate `APlayer` with `PlayerController` object.
struct PlayerControllerPair
{
if (allPlayers[i] == none) {
allPlayers.Remove(i, 1);
}
else {
i += 1;
}
}
}
var APlayer player;
var PlayerController controller;
};
// Store all known players along with their `PlayerController`s.
var private array<PlayerControllerPair> allPlayers;
// Shortcut to `ConnectionEvents`, so that we don't have to write
// `class'ConnectionEvents'` every time.
var const class<PlayerEvents> events;
/**
* Creates a new `APlayer` instance for a given `newPlayerController`
@ -52,26 +49,27 @@ private final function RemoveNonePlayers()
public final function bool RegisterPlayer(PlayerController newPlayerController)
{
local int i;
local APlayer newPlayer;
local Text textIdHash;
local PlayerControllerPair newPair;
if (newPlayerController == none) return false;
RemoveNonePlayers();
UpdateAllPlayers();
for (i = 0; i < allPlayers.length; i += 1)
{
if (allPlayers[i] == none) continue;
if (allPlayers[i].GetController() == newPlayerController) {
if (allPlayers[i].controller == newPlayerController) {
return false;
}
}
newPlayer = APlayer(_.memory.Allocate(class'APlayer'));
if (newPlayer == none)
{
_.logger.Fatal("Cannot spawn a new instance of `APlayer`."
@ "Acedia will not properly work from now on.");
return false;
}
newPlayer.Initialize(newPlayerController);
allPlayers[allPlayers.length] = newPlayer;
// Record new pair in service's data
newPair.controller = newPlayerController;
newPair.player = APlayer(_.memory.Allocate(class'APlayer', true));
allPlayers[allPlayers.length] = newPair;
// Initialize new `APlayer`
textIdHash = _.text.FromString(newPlayerController.GetPlayerIDHash());
newPair.player.Initialize(textIdHash);
textIdHash.FreeSelf();
// Run events
events.static.CallPlayerConnected(newPair.player);
return true;
}
@ -83,8 +81,59 @@ public final function bool RegisterPlayer(PlayerController newPlayerController)
*/
public final function array<APlayer> GetAllPlayers()
{
RemoveNonePlayers();
return allPlayers;
local int i;
local array<APlayer> result;
for (i = 0; i < allPlayers.length; i += 1)
{
if (allPlayers[i].controller != none) {
result[result.length] = allPlayers[i].player;
}
}
return result;
}
/**
* Returns `APlayer` associated with a given `PlayerController`.
*
* @param controller Controller for which we want to find associated player.
* @return `APlayer` that is associated with a given `PlayerController`.
* Can return `none` if player has already "expired".
*/
public final function APlayer GetPlayer(PlayerController controller)
{
local int i;
if (controller == none) {
return none;
}
for (i = 0; i < allPlayers.length; i += 1)
{
if (controller == allPlayers[i].controller) {
return allPlayers[i].player;
}
}
return none;
}
/**
* Returns `PlayerController` associated with a given `APlayer`.
*
* @param player Player for which we want to find associated controller.
* @return Controller that is associated with a given player.
* Can return `none` if controller has already "expired".
*/
public final function PlayerController GetController(APlayer player)
{
local int i;
if (player == none) {
return none;
}
for (i = 0; i < allPlayers.length; i += 1)
{
if (player == allPlayers[i].player) {
return allPlayers[i].controller;
}
}
return none;
}
/**
@ -97,15 +146,21 @@ public final function array<APlayer> GetAllPlayers()
public final function UpdateAllPlayers()
{
local int i;
RemoveNonePlayers();
for (i = 0; i < allPlayers.length; i += 1)
while (i < allPlayers.length)
{
if (allPlayers[i].controller == none)
{
if (allPlayers[i] != none){
allPlayers[i].Update();
events.static.CallPlayerDisconnected(allPlayers[i].player);
allPlayers.Remove(i, 1);
}
else {
i += 1;
}
}
}
defaultproperties
{
events = class'PlayerEvents'
requiredListeners(0) = class'ConnectionListener_Player'
}

4
sources/Service.uc

@ -1,7 +1,7 @@
/**
* Parent class for all services used in Acedia.
* Currently simply makes itself server-only.
* Copyright 2020 Anton Tarasenko
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -33,7 +33,7 @@ public static final function Service Require()
return Service(GetInstance());
}
default.blockSpawning = false;
newInstance = __().Spawn(default.class);
newInstance = Service(__().memory.Allocate(default.class));
default.blockSpawning = true;
return newInstance;
}

13
sources/Singleton.uc

@ -3,7 +3,7 @@
* that allows for only one instance of it to exist.
* To make sure your child class properly works, either don't overload
* 'PreBeginPlay' or make sure to call it's parent's version.
* Copyright 2019 Anton Tarasenko
* Copyright 2019 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -41,7 +41,7 @@ public final static function Singleton GetInstance(optional bool spawnIfMissing)
return default.activeInstance;
}
if (spawnIfMissing) {
return __().Spawn(default.class);
return Singleton(__().memory.Allocate(default.class));
}
return none;
}
@ -70,17 +70,14 @@ protected function OnDestroyed(){}
// |___________________________________________________________________________
event PreBeginPlay()
{
super.PreBeginPlay();
if (default.blockSpawning || GetInstance() != none)
{
if (default.blockSpawning || GetInstance() != none) {
Destroy();
return;
}
else
{
default.activeInstance = self;
super.PreBeginPlay();
OnCreated();
}
}
// Make sure only one instance of 'Singleton' exists at any point in time.
// Instead of overloading this function we suggest you overload a special

2
sources/Testing/TestCase.uc

@ -201,7 +201,7 @@ public final static function string GetGroup()
public final static function bool PerformTests()
{
default.finishedTests = false;
_().memory.Free(default.currentSummary);
__().memory.Free(default.currentSummary);
default.currentSummary = new class'TestCaseSummary';
default.currentSummary.Initialize(default.class);
TESTS();

1097
sources/Text/JSON/JSONAPI.uc

File diff suppressed because it is too large Load Diff

356
sources/Text/MutableText.uc

@ -0,0 +1,356 @@
/**
* Mutable version of Acedia's `Text`
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MutableText extends Text;
// Every formatted `string` essentially consists of multiple differently
// formatted (colored) parts. Such `string`s will be more convenient for us to
// work with if we separate them from each other.
// This structure represents one such block: maximum uninterrupted
// substring, every character of which has identical formatting.
// Do note that a single block does not define text formatting, -
// it is defined by the whole sequence of blocks before it
// (if `isOpening == false` you only know that you should change previous
// formatting, but you do not know to what).
struct FormattedBlock
{
// Did this block start by opening or closing formatted part?
// Ignored for the very first block without any formatting.
var bool isOpening;
// Full text inside the block, without any formatting
var array<int> contents;
// Formatting tag for this block
// (ignored for `isOpening == false`)
var string tag;
// Whitespace symbol that separates tag from the `contents`;
// For the purposes of reassembling a `string` broken into blocks.
// (ignored for `isOpening == false`)
var Character delimiter;
};
// Appending formatted `string` into the `MutableText` first requires to
// split it into series of `FormattedBlock` and then extract code points with
// the proper formatting from it.
// This variable contains intermediary data.
var array<FormattedBlock> splitBlocks;
// Formatted `string` can have an arbitrary level of folded format definitions,
// this array is used as a stack to keep track of opened formatting blocks
// when appending formatted `string`.
var array<Formatting> formattingStack;
/**
* Clears all current data from the caller `MutableText` instance.
*
* @return Returns caller `MutableText` to allow for method chaining.
*/
public final function MutableText Clear()
{
DropCodePoints();
return self;
}
/**
* Appends a new character to the caller `MutableText`.
*
* @param newCharacter Character to add to the caller `MutableText`.
* Only valid characters will be added.
* @return Caller `MutableText` to allow for method chaining.
*/
public final function MutableText AppendCharacter(Text.Character newCharacter)
{
if (!_.text.IsValidCharacter(newCharacter)) {
return self;
}
SetFormatting(newCharacter.formatting);
return MutableText(AppendCodePoint(newCharacter.codePoint));
}
/**
* Converts caller `MutableText` instance into lower case.
*/
public final function ToLower()
{
ConvertCase(true);
}
/**
* Converts caller `MutableText` instance into upper case.
*/
public final function ToUpper()
{
ConvertCase(false);
}
/**
* Appends contents of another `Text` to the caller `MutableText`.
*
* @param other Instance of `Text`, which content method must
* append. Appends nothing if passed value is `none`.
* @param defaultFormatting Formatting to apply to `other`'s character that
* do not have it specified. For example, `defaultFormatting.isColored`,
* but some of `other`'s characters do not have a color defined -
* they will be appended with a specified color.
* @return Caller `MutableText` to allow for method chaining.
*/
public final function MutableText Append(
Text other,
optional Formatting defaultFormatting)
{
local int i;
local int otherLength;
local Character nextCharacter;
local Formatting newFormatting;
if (other == none) {
return self;
}
SetFormatting(defaultFormatting);
otherLength = other.GetLength();
for (i = 0; i < otherLength; i += 1)
{
nextCharacter = other.GetRawCharacter(i);
if (other.IsFormattingChangedAt(i))
{
newFormatting = other.GetFormatting(i);
// If default formatting is specified, but `other`'s formatting
// (at least for some characters) is not, - apply default one
if (defaultFormatting.isColored && !newFormatting.isColored)
{
newFormatting.isColored = true;
newFormatting.color = defaultFormatting.color;
}
SetFormatting(newFormatting);
}
AppendCodePoint(nextCharacter.codePoint);
}
return self;
}
/**
* Appends contents of the plain `string` to the caller `MutableText`.
*
* @param source Plain `string` to be appended to
* the caller `MutableText`.
* @param defaultFormatting Formatting to be used for `source`'s characters.
* By default defines 'null' formatting (no color set).
* @return Caller `MutableText` to allow for method chaining.
*/
public final function MutableText AppendPlainString(
string source,
optional Formatting defaultFormatting)
{
local int i;
local int sourceLength;
sourceLength = Len(source);
SetFormatting(defaultFormatting);
// Decompose `source` into integer codes
for (i = 0; i < sourceLength; i += 1) {
AppendCodePoint(Asc(Mid(source, i, 1)));
}
return self;
}
/**
* Appends contents of the colored `string` to the caller `MutableText`.
*
* @param source Colored `string` to be appended to
* the caller `MutableText`.
* @param defaultFormatting Formatting to be used for `source`'s characters
* that have no color information defined.
* By default defines 'null' formatting (no color set).
* @return Caller `MutableText` to allow for method chaining.
*/
public final function MutableText AppendColoredString(
string source,
optional Formatting defaultFormatting)
{
local int i;
local int sourceLength;
local array<int> sourceAsIntegers;
local Formatting newFormatting;
// Decompose `source` into integer codes
sourceLength = Len(source);
for (i = 0; i < sourceLength; i += 1) {
sourceAsIntegers[sourceAsIntegers.length] = Asc(Mid(source, i, 1));
}
// With colored strings we only need to care about color for formatting
i = 0;
newFormatting = defaultFormatting;
SetFormatting(newFormatting);
while (i < sourceLength)
{
if (sourceAsIntegers[i] == CODEPOINT_ESCAPE)
{
if (i + 3 >= sourceLength) break;
newFormatting.isColored = true;
newFormatting.color = _.color.RGB(sourceAsIntegers[i + 1],
sourceAsIntegers[i + 2],
sourceAsIntegers[i + 3]);
i += 4;
SetFormatting(newFormatting);
}
else
{
AppendCodePoint(sourceAsIntegers[i]);
i += 1;
}
}
return self;
}
/**
* Appends contents of the formatted `string` to the caller `MutableText`.
*
* @param source Formatted `string` to be appended to
* the caller `MutableText`.
* @param defaultFormatting Formatting to be used for `source`'s characters
* that have no color information defined.
* @return Caller `MutableText` to allow for method chaining.
*/
public final function MutableText AppendFormattedString(
string source,
optional Formatting defaultFormatting)
{
local int i;
local Parser parser;
SplitFormattedStringIntoBlocks(source);
if (splitBlocks.length <= 0) {
return self;
}
SetupFormattingStack(defaultFormatting);
parser = Parser(_.memory.Allocate(class'Parser'));
// First element of `decomposedSource` is special and has
// no color information,
// see `SplitFormattedStringIntoBlocks()` for details.
SetFormatting(defaultFormatting);
AppendManyCodePoints(splitBlocks[0].contents);
for (i = 1; i < splitBlocks.length; i += 1)
{
if (splitBlocks[i].isOpening)
{
parser.InitializeS(splitBlocks[i].tag);
SetFormatting(PushIntoFormattingStack(parser));
}
else {
SetFormatting(PopFormattingStack());
}
AppendManyCodePoints(splitBlocks[i].contents);
}
_.memory.Free(parser);
return self;
}
// Function that breaks formatted string into array of `FormattedBlock`s.
// Returned array is guaranteed to always have at least one block.
// First block in array always corresponds to part of the input string
// (`source`) without any formatting defined, even if it's empty.
// This is to avoid `FormattedBlock` having a third option besides two defined
// by `isOpening` variable.
private final function SplitFormattedStringIntoBlocks(string source)
{
local Parser parser;
local Character nextCharacter;
local FormattedBlock nextBlock;
splitBlocks.length = 0;
parser = _.text.ParseString(source);
while (!parser.HasFinished()) {
parser.MCharacter(nextCharacter);
// New formatted block by "{<color>"
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT))
{
splitBlocks[splitBlocks.length] = nextBlock;
nextBlock = CreateFormattedBlock(true);
parser.MUntilS(nextBlock.tag,, true)
.MCharacter(nextBlock.delimiter);
if (!parser.Ok()) {
break;
}
continue;
}
// New formatted block by "}"
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT))
{
splitBlocks[splitBlocks.length] = nextBlock;
nextBlock = CreateFormattedBlock(false);
continue;
}
// Escaped sequence
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_FORMAT_ESCAPE)) {
parser.MCharacter(nextCharacter);
}
if (!parser.Ok()) {
break;
}
nextBlock.contents[nextBlock.contents.length] = nextCharacter.codePoint;
}
// Only put in empty block if there is nothing else.
if (nextBlock.contents.length > 0 || splitBlocks.length == 0) {
splitBlocks[splitBlocks.length] = nextBlock;
}
_.memory.Free(parser);
}
// Following two functions are to maintain a "color stack" that will
// remember unclosed colors (new colors are obtained from a parser) defined in
// formatted string, on order.
// Stack array always contains one element, defined by
// the `SetupFormattingStack()` call. It corresponds to the default formatting
// that will be used when we pop all the other elements.
// It is necessary to deal with possible folded formatting definitions in
// formatted strings.
// For storing the color information we simply use `Text.Character`,
// ignoring all information that is not related to colors.
private final function SetupFormattingStack(Text.Formatting defaultFormatting)
{
formattingStack.length = 0;
formattingStack[0] = defaultFormatting;
}
private final function Formatting PushIntoFormattingStack(
Parser formattingDefinitionParser)
{
local Formatting newFormatting;
if (_.color.ParseWith(formattingDefinitionParser, newFormatting.color)) {
newFormatting.isColored = true;
}
formattingStack[formattingStack.length] = newFormatting;
return newFormatting;
}
private final function Formatting PopFormattingStack()
{
local Formatting result;
formattingStack.length = Max(1, formattingStack.length - 1);
if (formattingStack.length > 0) {
result = formattingStack[formattingStack.length - 1];
}
return result;
}
// Helper method for a quick creation of a new `FormattedBlock`
private final function FormattedBlock CreateFormattedBlock(bool isOpening)
{
local FormattedBlock newBlock;
newBlock.isOpening = isOpening;
return newBlock;
}
defaultproperties
{
}

631
sources/Text/Parser.uc

File diff suppressed because it is too large Load Diff

501
sources/Text/Tests/TEST_JSON.uc

@ -0,0 +1,501 @@
/**
* Set of tests for functionality of JSON printing/parsing.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_JSON extends TestCase
abstract;
var string simpleJSONObject, complexJSONObject;
protected static function TESTS()
{
Test_Print();
Test_Parse();
}
protected static function Test_Print()
{
Context("Testing printing simple JSON values.");
SubTest_SimplePrint();
SubTest_ArrayPrint();
}
protected static function SubTest_SimplePrint()
{
local string complexString;
Issue("Simple JSON values are not printed as expected.");
TEST_ExpectTrue(__().json.Print(none).ToPlainString() == "null");
TEST_ExpectTrue( __().json.Print(__().box.bool(false)).ToPlainString()
== "false");
TEST_ExpectTrue( __().json.Print(__().ref.bool(true)).ToFormattedString()
== "true");
TEST_ExpectTrue( __().json.Print(__().box.int(-752)).ToFormattedString()
== "-752");
TEST_ExpectTrue( __().json.Print(__().ref.int(36235)).ToFormattedString()
== "36235");
TEST_ExpectTrue( __().json.Print(__().box.float(5.673)).ToPlainString()
== "5.673");
TEST_ExpectTrue( __().json.Print(__().ref.float(-3.502)).ToPlainString()
== "-3.502");
TEST_ExpectTrue( __().json.Print(F("{#ff000 col}ored")).ToPlainString()
== "\"colored\"");
TEST_ExpectTrue( __().json.Print(P("simple text")).ToFormattedString()
== "\"simple text\"");
complexString = "\"comp/lex\"" $ Chr(0x0a) $ "\\str";
TEST_ExpectTrue( __().json.Print(P(complexString)).ToFormattedString()
== "\"\\\"comp\\/lex\\\"\\n\\\\str\"");
Issue("Printing unrelated objects does not produce `none`s.");
TEST_ExpectNone(
__().json.Print(AcediaObject(__().memory.Allocate(class'Parser'))));
}
protected static function SubTest_ArrayPrint()
{
local DynamicArray array, subArray;
array = DynamicArray(__().memory.Allocate(class'DynamicArray'));
subArray = DynamicArray(__().memory.Allocate(class'DynamicArray'));
subArray.AddItem(__().box.int(-752));
subArray.AddItem(__().ref.bool(true));
subArray.AddItem(__().box.float(3.44));
subArray.AddItem(__().text.FromString("\"quoted text\""));
array.AddItem(__().ref.float(34.1));
array.AddItem(none);
array.AddItem(subArray);
array.AddItem(__().text.FromString(" "));
Issue("JSON arrays are not printed as expected.");
TEST_ExpectTrue(
__().json.PrintArray(array).ToPlainString()
== "[34.1,null,[-752,true,3.44,\"\\\"quoted text\\\"\"],\"\\t\"]");
TEST_ExpectTrue(
__().json.Print(array).ToPlainString()
== "[34.1,null,[-752,true,3.44,\"\\\"quoted text\\\"\"],\"\\t\"]");
}
protected static function Test_Parse()
{
Context("Testing JSON null parsing methods.");
SubTest_ParseNull();
Context("Testing JSON boolean parsing methods.");
SubTest_ParseBooleanVariable();
SubTest_ParseBoolean();
Context("Testing JSON number parsing methods.");
SubTest_ParseIntegerVariable();
SubTest_ParseFloatVariable();
SubTest_ParseNumber();
Context("Testing JSON string parsing methods.");
SubTest_ParseString();
Context("Testing JSON methods for parsing arrays"
@ "(on arrays with simple value).");
SubTest_ParseArraySuccess();
SubTest_ParseArrayFailure();
Context("Testing JSON methods for parsing object"
@ "(on objects with simple value).");
SubTest_ParseObjectSuccess();
SubTest_ParseObjectFailure();
Context("Testing generic JSON methods for value parsing.");
SubTest_ParseSimpleValueSuccess();
SubTest_ParseSimpleValueFailure();
Context("Testing parsing complex JSON values.");
SubTest_ParseComplex();
}
protected static function SubTest_ParseNull()
{
local Parser parser;
Issue("`IsNull()` returns `false` for correct JSON null values.");
TEST_ExpectTrue(__().json.IsNull(P("Null")));
TEST_ExpectTrue(__().json.IsNull(P("nUll")));
Issue("`ParseBoolean()` returns `true` for invalid JSON null values.");
TEST_ExpectFalse(__().json.IsNull(P("Nul")));
TEST_ExpectFalse(__().json.IsNull(P(" nUll")));
TEST_ExpectFalse(__().json.IsNull(P("Null ")));
parser = __().text.Parse(P("nullNullnU"));
Issue("`TryNullWith()` cannot parse correct JSON null values.");
__().json.TryNullWith(parser);
__().json.TryNullWith(parser);
TEST_ExpectTrue(parser.Ok());
__().json.TryNullWith(parser);
TEST_ExpectFalse(parser.Ok());
}
protected static function SubTest_ParseBooleanVariable()
{
local Parser parser;
parser = __().text.Parse(P("trUEfAlseTr"));
Issue("`ParseBooleanVariableWith()` cannot parse correct"
@ "JSON boolean values.");
TEST_ExpectTrue(__().json.ParseBooleanVariableWith(parser));
TEST_ExpectFalse(__().json.ParseBooleanVariableWith(parser));
TEST_ExpectTrue(parser.Ok());
Issue("`ParseBooleanVariableWith()` returns `true` for invalid"
@ "JSON boolean values.");
TEST_ExpectFalse(__().json.ParseBooleanVariableWith(parser));
Issue("`ParseBooleanVariableWith()` reports success for invalid"
@ "JSON boolean values.");
TEST_ExpectFalse(parser.Ok());
}
protected static function SubTest_ParseBoolean()
{
local Parser parser;
Issue("`ParseBoolean()` fails to parse correct JSON booleans.");
TEST_ExpectTrue(BoolBox(__().json.ParseBoolean(P("tRuE"))).Get());
TEST_ExpectFalse(BoolRef(__().json.ParseBoolean(P("FAlSe"), true)).Get());
Issue("`ParseBoolean()` returns non-`none` values for invalid"
@ "JSON booleans.");
TEST_ExpectNone(__().json.ParseBoolean(P("tru")));
TEST_ExpectNone(__().json.ParseBoolean(P("")));
TEST_ExpectNone(__().json.ParseBoolean(P("false+")));
parser = __().text.Parse(P("trUEfAlseTr"));
Issue("`ParseBooleanWith()` fails to parse correct JSON booleans.");
TEST_ExpectTrue(BoolBox(__().json.ParseBooleanWith(parser)).Get());
TEST_ExpectFalse(BoolRef(__().json.ParseBooleanWith(parser, true)).Get());
TEST_ExpectTrue(parser.Ok());
Issue("`ParseBooleanWith()` returns non-`none` values for invalid"
@ "JSON booleans or parsers in failed state.");
TEST_ExpectNone(__().json.ParseBooleanWith(parser));
TEST_ExpectFalse(parser.Ok());
TEST_ExpectNone(__().json.ParseBooleanWith(parser));
}
protected static function SubTest_ParseIntegerVariable()
{
local Parser parser;
parser = __().text.ParseString("13 -67.3 423e-2 0x67");
Issue("`ParseIntegerVariableWith()` cannot parse correct"
@ "JSON number values.");
TEST_ExpectTrue(__().json.ParseIntegerVariableWith(parser) == 13);
TEST_ExpectTrue(__().json.ParseIntegerVariableWith(parser.Skip()) == -67);
TEST_ExpectTrue(__().json.ParseIntegerVariableWith(parser.Skip()) == 4);
TEST_ExpectTrue(__().json.ParseIntegerVariableWith(parser.Skip()) == 0);
TEST_ExpectTrue(parser.Ok());
Issue("`ParseIntegerVariableWith()` returns non-zero values when it"
@ "should have failed parsing.");
TEST_ExpectTrue(__().json.ParseIntegerVariableWith(parser.Skip()) == 0);
Issue("`ParseIntegerVariableWith()` does not put parser into a failed state"
@ "when it failed parsing.");
TEST_ExpectFalse(parser.Ok());
Issue("`ParseIntegerVariableWith()` parses number values in"
@ "\"float format\" with `integerOnly` parameter set to `true`.");
parser = __().text.ParseString("-67.3");
TEST_ExpectTrue(__().json.ParseIntegerVariableWith(parser, true) == 0);
TEST_ExpectFalse(parser.Ok());
parser = __().text.ParseString("32e2");
TEST_ExpectTrue(__().json.ParseIntegerVariableWith(parser, true) == 0);
TEST_ExpectFalse(parser.Ok());
}
protected static function SubTest_ParseFloatVariable()
{
local Parser parser;
parser = __().text.ParseString("13 -67.3 423e-2 0x67");
Issue("`ParseFloatVariableWith()` cannot parse correct JSON"
@ "number values.");
TEST_ExpectTrue(__().json.ParseFloatVariableWith(parser) == 13);
TEST_ExpectTrue(__().json.ParseFloatVariableWith(parser.Skip()) == -67.3);
TEST_ExpectTrue(__().json.ParseFloatVariableWith(parser.Skip()) == 4.23);
TEST_ExpectTrue(__().json.ParseFloatVariableWith(parser.Skip()) == 0);
TEST_ExpectTrue(parser.Ok());
Issue("`ParseFloatVariableWith()` returns non-zero values when it"
@ "should have failed parsing.");
TEST_ExpectTrue(__().json.ParseFloatVariableWith(parser.Skip()) == 0);
Issue("`ParseFloatVariableWith()` does not put parser into a failed state"
@ "when it failed parsing.");
TEST_ExpectFalse(parser.Ok());
}
protected static function SubTest_ParseNumber()
{
local Parser parser;
Issue("`ParseNumber()` fails to parse correct JSON numbers.");
TEST_ExpectTrue(IntBox(__().json.ParseNumber(P("32"))).Get() == 32);
TEST_ExpectTrue( IntRef(__().json.ParseNumber(P("-24"), true)).Get()
== -24);
TEST_ExpectTrue( FloatBox(__().json.ParseNumber(P("-2.6e3"))).Get()
== -2600);
TEST_ExpectTrue( FloatRef(__().json.ParseNumber(P("98e-1"), true)).Get()
== 9.8);
Issue("`ParseNumber()` returns non-`none` values for invalid"
@ "JSON numbers.");
TEST_ExpectNone(__().json.ParseNumber(P(".34")));
TEST_ExpectNone(__().json.ParseNumber(P("4 ")));
parser = __().text.Parse(P("-83 0 0.4 -4.676 e2"));
Issue("`ParseNumberWith()` fails to parse correct JSON numbers.");
TEST_ExpectTrue( IntBox(__().json.ParseNumberWith(parser.Skip())).Get()
== -83);
TEST_ExpectTrue(
IntRef(__().json.ParseNumberWith(parser.Skip(), true)).Get() == 0);
TEST_ExpectTrue( FloatBox(__().json.ParseNumberWith(parser.Skip())).Get()
== 0.4);
TEST_ExpectTrue(
FloatRef(__().json.ParseNumberWith(parser.Skip(), true)).Get()
== -4.676);
TEST_ExpectTrue(parser.Ok());
Issue("`ParseNumberWith()` returns non-`none` values for invalid"
@ "JSON numbers or parsers in failed state.");
TEST_ExpectNone(__().json.ParseNumberWith(parser.Skip()));
TEST_ExpectFalse(parser.Ok());
TEST_ExpectNone(__().json.ParseNumberWith(parser));
}
protected static function SubTest_ParseString()
{
local Parser parser;
Issue("`ParseString()` fails to parse correct JSON strings.");
TEST_ExpectTrue( __().json.ParseString(P("\"string !\"")).ToPlainString()
== "string !");
TEST_ExpectTrue(
MutableText(__().json.ParseString(P("\"\""), true)).ToPlainString()
== "");
Issue("`ParseString()` returns non-`none` values for invalid"
@ "JSON strings.");
TEST_ExpectNone(__().json.ParseString(P("\"unclosed")));
TEST_ExpectNone(__().json.ParseString(P("no quotes")));
TEST_ExpectNone(__().json.ParseString(P("\"space at the end\" ")));
parser = __().text.Parse(P("\"str\"\" also a kind `of` a string\"not"));
Issue("`ParseStringWith()` fails to parse correct JSON strings.");
TEST_ExpectTrue(__().json.ParseStringWith(parser).ToPlainString() == "str");
TEST_ExpectTrue(
MutableText(__().json.ParseStringWith(parser, true)).ToPlainString()
== " also a kind `of` a string");
TEST_ExpectTrue(parser.Ok());
Issue("`ParseStringWith()` returns non-`none` values for invalid"
@ "JSON strings or parsers in failed state.");
TEST_ExpectNone(__().json.ParseStringWith(parser));
TEST_ExpectFalse(parser.Ok());
TEST_ExpectNone(__().json.ParseStringWith(parser));
}
protected static function SubTest_ParseArraySuccess()
{
local Parser parser;
local DynamicArray result;
Issue("`ParseArrayWith()` fails to parse empty JSON array.");
parser = __().text.ParseString("[]");
result = __().json.ParseArrayWith(parser);
TEST_ExpectNotNone(result);
TEST_ExpectTrue(parser.OK());
TEST_ExpectTrue(result.GetLength() == 0);
Issue("`ParseArrayWith()` fails to parse correct JSON arrays"
@ "(as immutable).");
parser = __().text.ParseString("[true, 76.4, \"val\", null, 5]");
result = __().json.ParseArrayWith(parser);
TEST_ExpectNotNone(result);
TEST_ExpectTrue(parser.OK());
TEST_ExpectTrue(result.GetLength() == 5);
TEST_ExpectTrue(BoolBox(result.GetItem(0)).Get());
TEST_ExpectTrue(FloatBox(result.GetItem(1)).Get() == 76.4);
TEST_ExpectTrue(Text(result.GetItem(2)).ToPlainString() == "val");
TEST_ExpectNone(result.GetItem(3));
TEST_ExpectTrue(IntBox(result.GetItem(4)).Get() == 5);
Issue("`ParseArrayWith()` fails to parse correct JSON arrays"
@ "(as mutable).");
result = __().json.ParseArrayWith(parser.R(), true);
TEST_ExpectNotNone(result);
TEST_ExpectTrue(parser.OK());
TEST_ExpectTrue(result.GetLength() == 5);
TEST_ExpectTrue(BoolRef(result.GetItem(0)).Get());
TEST_ExpectTrue(FloatRef(result.GetItem(1)).Get() == 76.4);
TEST_ExpectTrue(MutableText(result.GetItem(2)).ToPlainString() == "val");
TEST_ExpectNone(result.GetItem(3));
TEST_ExpectTrue(IntRef(result.GetItem(4)).Get() == 5);
}
protected static function SubTest_ParseArrayFailure()
{
local Parser parser;
local DynamicArray result;
Issue("`ParseArrayWith()` incorrectly handles parsing invalid"
@ "JSON arrays.");
parser = __().text.ParseString("[,]");
result = __().json.ParseArrayWith(parser);
TEST_ExpectNone(result);
TEST_ExpectFalse(parser.OK());
parser = __().text.ParseString("[true, 76.4, \"val\", null, 5");
result = __().json.ParseArrayWith(parser);
TEST_ExpectNone(result);
TEST_ExpectFalse(parser.OK());
parser = __().text.ParseString("[true, 76.4, \"val\", null,]");
result = __().json.ParseArrayWith(parser, true);
TEST_ExpectNone(result);
TEST_ExpectFalse(parser.OK());
}
protected static function SubTest_ParseSimpleValueSuccess()
{
local JSONAPI api;
local Parser parser;
api = __().json;
Issue("`ParseWith()` fails to parse correct JSON values.");
parser = __().text.ParseString("false, 98.2, 42, \"hmmm\", null");
TEST_ExpectFalse(BoolBox(api.ParseWith(parser)).Get());
parser.MatchS(",").Skip();
TEST_ExpectTrue(FloatBox(api.ParseWith(parser)).Get() == 98.2);
parser.MatchS(",").Skip();
TEST_ExpectTrue(IntRef(api.ParseWith(parser, true)).Get() == 42);
parser.MatchS(",").Skip();
TEST_ExpectTrue(
MutableText(api.ParseWith(parser, true)).ToPlainString()
== "hmmm");
parser.MatchS(",").Skip();
TEST_ExpectNone(api.ParseWith(parser));
TEST_ExpectTrue(parser.Ok());
}
protected static function SubTest_ParseSimpleValueFailure()
{
local JSONAPI api;
local Parser parser;
api = __().json;
Issue("`ParseWith()` does not correctly handle parsing invalid"
@ "JSON values.");
parser = __().text.ParseString("tru");
TEST_ExpectNone(api.ParseWith(parser));
TEST_ExpectFalse(parser.Ok());
parser = __().text.ParseString("");
TEST_ExpectNone(api.ParseWith(parser));
TEST_ExpectFalse(parser.Ok());
parser = __().text.ParseString("NUL");
TEST_ExpectNone(api.ParseWith(parser));
TEST_ExpectFalse(parser.Ok());
}
protected static function SubTest_ParseObjectSuccess()
{
local Parser parser;
local AssociativeArray result;
Issue("`ParseObjectWith()` fails to parse empty JSON object.");
parser = __().text.ParseString("{ }");
result = __().json.ParseObjectWith(parser);
TEST_ExpectNotNone(result);
TEST_ExpectTrue(parser.OK());
TEST_ExpectTrue(result.GetLength() == 0);
Issue("`ParseObjectWith()` fails to parse correct JSON objects"
@ "(as immutable).");
parser = __().text.ParseString(default.simpleJSONObject);
result = __().json.ParseObjectWith(parser);
TEST_ExpectNotNone(result);
TEST_ExpectTrue(parser.OK());
TEST_ExpectTrue(result.GetLength() == 4);
TEST_ExpectTrue(IntBox(result.GetItem(P("var"))).Get() == 13);
TEST_ExpectTrue(BoolBox(result.GetItem(P("another"))).Get() == true);
TEST_ExpectTrue( Text(result.GetItem(P("string one"))).ToPlainString()
== "string!");
TEST_ExpectNone(MutableText(result.GetItem(P("string one"))));
TEST_ExpectNone(result.GetItem(P("last")));
Issue("`ParseObjectWith()` fails to parse correct JSON objects"
@ "(as mutable).");
result = __().json.ParseObjectWith(parser.R(), true);
TEST_ExpectNotNone(result);
TEST_ExpectTrue(IntRef(result.GetItem(P("var"))).Get() == 13);
TEST_ExpectTrue(BoolRef(result.GetItem(P("another"))).Get() == true);
TEST_ExpectTrue(
MutableText(result.GetItem(P("string one"))).ToPlainString()
== "string!");
TEST_ExpectNone(result.GetItem(P("last")));
}
protected static function SubTest_ParseObjectFailure()
{
local Parser parser;
local AssociativeArray result;
Issue("`ParseObjectWith()` incorrectly handles parsing invalid"
@ "JSON objects.");
parser = __().text.ParseString("{,}");
result = __().json.ParseObjectWith(parser);
TEST_ExpectNone(result);
TEST_ExpectFalse(parser.OK());
parser = __().text.ParseString("{var:null}");
result = __().json.ParseObjectWith(parser);
TEST_ExpectNone(result);
TEST_ExpectFalse(parser.OK());
parser = __().text.ParseString("{\"var\":57,}");
result = __().json.ParseObjectWith(parser);
TEST_ExpectNone(result);
TEST_ExpectFalse(parser.OK());
parser = __().text.ParseString("{,\"var\":true}");
result = __().json.ParseObjectWith(parser);
TEST_ExpectNone(result);
TEST_ExpectFalse(parser.OK());
}
protected static function SubTest_ParseComplex()
{
local Parser parser;
local DynamicArray subArr;
local AssociativeArray root, mainObj, subObj, inner;
Issue("`ParseObjectWith()` cannot handle complex values.");
parser = __().text.ParseString(default.complexJSONObject);
root = AssociativeArray(__().json.ParseWith(parser));
TEST_ExpectTrue(root.GetLength() == 3);
TEST_ExpectTrue(FloatBox(root.GetItem(P("some_var"))).Get() == -7.32);
TEST_ExpectTrue( Text(root.GetItem(P("another_var"))).ToPlainString()
== "aye!");
mainObj = AssociativeArray(root.GetItem(P("innerObject")));
TEST_ExpectTrue(root.GetLength() == 3);
TEST_ExpectTrue(BoolBox(mainObj.GetItem(P("my_bool"))).Get() == true);
TEST_ExpectTrue(IntBox(mainObj.GetItem(P("my_int"))).Get() == -9823452);
subObj = AssociativeArray(mainObj.GetItem(P("one more")));
subArr = DynamicArray(mainObj.GetItem(P("array")));
TEST_ExpectTrue(subObj.GetLength() == 3);
TEST_ExpectTrue(IntBox(subObj.GetItem(P("nope"))).Get() == 324532);
TEST_ExpectTrue(BoolBox(subObj.GetItem(P("whatever"))).Get() == false);
TEST_ExpectTrue( Text(subObj.GetItem(P("o rly?"))).ToPlainString()
== "ya rly");
inner = AssociativeArray(subArr.GetItem(3));
TEST_ExpectTrue(Text(subArr.GetItem(0)).ToPlainString() == "Engine.Actor");
TEST_ExpectTrue(BoolBox(subArr.GetItem(1)).Get() == false);
TEST_ExpectNone(subArr.GetItem(2));
TEST_ExpectTrue(FloatBox(subArr.GetItem(4)).Get() == 56.6);
TEST_ExpectTrue(
Text(inner.GetItem(P("something \"here\""))).ToPlainString()
== "yes");
TEST_ExpectTrue(FloatBox(inner.GetItem(P("maybe"))).Get() == 0.003);
}
defaultproperties
{
caseName = "JSON"
caseGroup = "Text"
simpleJSONObject = "{\"var\": 13, \"another\": true , \"string one\": \"string!\",\"last\": null}"
complexJSONObject = "{\"innerObject\":{\"my_bool\":true,\"array\":[\"Engine.Actor\",false,null,{\"something \\\"here\\\"\":\"yes\",\"maybe\":0.003},56.6],\"one more\":{\"nope\":324532,\"whatever\":false,\"o rly?\":\"ya rly\"},\"my_int\":-9823452},\"some_var\":-7.32,\"another_var\":\"aye!\"}"
}

BIN
sources/Text/Tests/TEST_Parser.uc

Binary file not shown.

BIN
sources/Text/Tests/TEST_Text.uc

Binary file not shown.

BIN
sources/Text/Tests/TEST_TextAPI.uc

Binary file not shown.

150
sources/Text/Tests/TEST_TextCache.uc

@ -0,0 +1,150 @@
/**
* Set of tests for functionality of `TextCache` class.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_TextCache extends TestCase
abstract;
var string formatted1, formatted2, formatted3, formatted4;
protected static function TESTS()
{
Test_Plain();
Test_Formatted();
Test_Indexed();
}
protected static function Test_Plain()
{
local int lifeVersion;
local Text text1, text2, text3, newText;
local TextCache cache;
Context("Testing caching plain strings.");
cache = TextCache(__().memory.Allocate(class'TextCache'));
text1 = cache.GetPlainText("First string");
text2 = cache.GetPlainText("Second string");
text3 = cache.GetPlainText("Last string");
Issue("Cache returns `none`.");
TEST_ExpectNotNone(text1);
TEST_ExpectNotNone(text2);
TEST_ExpectNotNone(text3);
Issue("Cache returns different objects for the same `string`.");
TEST_ExpectTrue(text1 == cache.GetPlainText("First string"));
TEST_ExpectTrue(text2 == cache.GetPlainText("Second string"));
TEST_ExpectTrue(text3 == cache.GetPlainText("Last string"));
Issue("Cache returns dead, previously deallocated `Text` reference.");
lifeVersion = text1.GetLifeVersion();
text1.FreeSelf();
newText = cache.GetFormattedText(default.formatted1);
TEST_ExpectTrue( text1 != newText
|| lifeVersion != newText.GetLifeVersion());
Issue("Cache returns `Text` with wrong data.");
TEST_ExpectTrue( cache.GetPlainText("First string").ToPlainString()
== "First string");
TEST_ExpectTrue( cache.GetPlainText("New string").ToPlainString()
== "New string");
}
protected static function Test_Formatted()
{
local int lifeVersion;
local Text text1, text2, text3, newText;
local TextCache cache;
Context("Testing caching formatted strings.");
cache = TextCache(__().memory.Allocate(class'TextCache'));
text1 = cache.GetFormattedText(default.formatted1);
text2 = cache.GetFormattedText(default.formatted2);
text3 = cache.GetFormattedText(default.formatted3);
Issue("Cache returns `none`.");
TEST_ExpectNotNone(text1);
TEST_ExpectNotNone(text2);
TEST_ExpectNotNone(text3);
Issue("Cache returns different objects for the same `string`.");
TEST_ExpectTrue(text1 == cache.GetFormattedText(default.formatted1));
TEST_ExpectTrue(text2 == cache.GetFormattedText(default.formatted2));
TEST_ExpectTrue(text3 == cache.GetFormattedText(default.formatted3));
Issue("Cache returns dead, previously deallocated `Text` reference.");
lifeVersion = text1.GetLifeVersion();
text1.FreeSelf();
newText = cache.GetFormattedText(default.formatted1);
TEST_ExpectTrue( text1 != newText
|| lifeVersion != newText.GetLifeVersion());
Issue("Cache returns `Text` with wrong data.");
TEST_ExpectTrue(cache.GetFormattedText(default.formatted1)
.ToFormattedString() == default.formatted1);
TEST_ExpectTrue(cache.GetFormattedText(default.formatted4)
.ToFormattedString() == default.formatted4);
}
protected static function Test_Indexed()
{
local int lifeVersion;
local Text text1, text2, text3, newText;
local TextCache cache;
Context("Testing caching indexed strings.");
cache = TextCache(__().memory.Allocate(class'TextCache'));
text1 = cache.AddIndexedText(default.formatted1).GetIndexedText(0);
text2 = cache.AddIndexedText(default.formatted2).GetIndexedText(1);
text3 = cache.AddIndexedText(default.formatted3).GetIndexedText(2);
Issue("Cache returns `none`.");
TEST_ExpectNotNone(text1);
TEST_ExpectNotNone(text2);
TEST_ExpectNotNone(text3);
Issue("Cache returns different objects for the same index.");
TEST_ExpectTrue(text1 == cache.GetIndexedText(0));
TEST_ExpectTrue(text2 == cache.GetIndexedText(1));
TEST_ExpectTrue(text3 == cache.GetIndexedText(2));
Issue("Cache returns dead, previously deallocated `Text` reference.");
lifeVersion = text1.GetLifeVersion();
text1.FreeSelf();
newText = cache.GetIndexedText(0);
TEST_ExpectTrue( text1 != newText
|| lifeVersion != newText.GetLifeVersion());
Issue("Cache returns `Text` with wrong data.");
TEST_ExpectTrue(cache.GetIndexedText(0)
.ToFormattedString() == default.formatted1);
Issue("Cache does not return `none` for wrong indices.");
TEST_ExpectNone(cache.GetIndexedText(-1));
TEST_ExpectNone(cache.GetIndexedText(3));
}
defaultproperties
{
caseName = "TextCache"
caseGroup = "Text"
formatted1 = "{rgb(23,122,231) First} {rgb(255,0,0) string}"
formatted2 = "{rgb(32,1,154) Second} {rgb(0,255,0) string}"
formatted3 = "{rgb(76,23,111) Last} {rgb(0,0,255) string}"
formatted4 = "{rgb(145,231,41) New str}ing"
}

1228
sources/Text/Text.uc

File diff suppressed because it is too large Load Diff

1073
sources/Text/TextAPI.uc

File diff suppressed because it is too large Load Diff

208
sources/Text/TextCache.uc

@ -0,0 +1,208 @@
/**
* Auxiliary class for `AcediaObject` / `AcediaActor` that maps `string`s
* to `Text`s constant, returning same `Text` object for the equal `string`s
* (unless it was deallocated).
* This object solves the problem of `Acedia` needing a simple way to
* create the `Text` without the need to later deallocate it by reusing same
* `Text` instance for the same `string`s.
* It was implemented to provide a simple-to-use `string` -> `Text`
* conversion for a small amount of `string`s and should not be considered
* an efficient choice to cache large amounts of them.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TextCache extends AcediaObject;
// These arrays are supposed to be treated as an "unrolled" singular array
// of struct with three fields. It is done this way to avoid overhead/issues
// that come with arrays of `struct`s. This means we must take care to maintain
// invariant that these arrays have the same length.
// <type>Strings - `string` value in a cached pair
// <type>Text - `Text` value in a cached pair
// <type>Strings - life version of cached `Text` to check if
// stored instance was reallocated and needs to be recreated.
// Pairs for plain strings
var private array<string> plainStrings;
var private array<Text> plainTexts;
var private array<int> plainLifeVersions;
// Pairs for colored strings
var private array<string> coloredStrings;
var private array<Text> coloredTexts;
var private array<int> coloredLifeVersions;
// Pairs for formatted strings
var private array<string> formattedStrings;
var private array<Text> formattedTexts;
var private array<int> formattedLifeVersions;
// Pairs for indexed strings:
// for `Text`s to be obtained by index rather than `string` variable.
var private array<string> indexedStrings;
var private array<Text> indexedTexts;
var private array<int> indexedLifeVersions;
protected function Finalizer()
{
plainStrings.length = 0;
plainTexts.length = 0;
plainLifeVersions.length = 0;
coloredStrings.length = 0;
coloredTexts.length = 0;
coloredLifeVersions.length = 0;
formattedStrings.length = 0;
formattedTexts.length = 0;
formattedLifeVersions.length = 0;
}
/**
* Returns (immutable) `Text` object that stores given `string`.
*
* If caller `TextCache` has already been asked to return given `string`,
* it will attempt to return the same `Text` object (unless it
* was deallocated). Otherwise `TextCache` will create a new `Text`.
*
* @param string Plain `string` for returned `Text` to contain.
* @return `Text` that contains same data as a given plain `string`.
* Guaranteed to not be `none` and allocated.
*/
public final function Text GetPlainText(string string)
{
local int i;
local Text result;
// Check if we have already cached `string`
for (i = 0; i < plainStrings.length; i += 1)
{
// Skip all other `string`s
if (plainStrings[i] != string) continue;
// Replace cached `string` if it was deallocated externally
if (plainTexts[i].GetLifeVersion() != plainLifeVersions[i]) break;
return plainTexts[i];
}
// `i` is equal to array index where `string` must be cached.
// Normally it's an index of the new element (`plainTexts.length`),
// but can also contain already existing index if cached `Text`
// was invalidated.
result = _.text.FromString(string);
plainStrings[i] = string;
plainTexts[i] = result;
plainLifeVersions[i] = result.GetLifeVersion();
return result;
}
/**
* Returns (immutable) `Text` object that stores given `string`.
*
* If caller `TextCache` has already been asked to return given `string`,
* it will attempt to return the same `Text` object (unless it
* was deallocated). Otherwise `TextCache` will create a new `Text`.
*
* @param string Colored `string` for returned `Text` to contain.
* @return `Text` that contains same data as a given colored `string`.
* Guaranteed to not be `none` and allocated.
*/
public final function Text GetColoredText(string string)
{
local int i;
local Text result;
// Check if we have already cached `string`
for (i = 0; i < coloredStrings.length; i += 1)
{
// Skip all other `string`s
if (coloredStrings[i] != string) {
continue;
}
// Replace cached `string` if it was deallocated externally
if (coloredTexts[i].GetLifeVersion() != coloredLifeVersions[i]) {
break;
}
return coloredTexts[i];
}
// `i` is equal to array index where `string` must be cached.
// Normally it's an index of the new element (`coloredTexts.length`),
// but can also contain already existing index if cached `Text`
// was invalidated.
result = _.text.FromColoredString(string);
coloredStrings[i] = string;
coloredTexts[i] = result;
coloredLifeVersions[i] = result.GetLifeVersion();
return result;
}
/**
* Returns (immutable) `Text` object that stores given `string`.
*
* If caller `TextCache` has already been asked to return given `string`,
* it will attempt to return the same `Text` object (unless it
* was deallocated). Otherwise `TextCache` will create a new `Text`.
*
* @param string Formatted `string` for returned `Text` to contain.
* @return `Text` that contains same data as a given formatted `string`.
* Guaranteed to not be `none` and allocated.
*/
public final function Text GetFormattedText(string string)
{
local int i;
local Text result;
// Check if we have already cached `string`
for (i = 0; i < formattedStrings.length; i += 1)
{
// Skip all other `string`s
if (formattedStrings[i] != string) continue;
// Replace cached `string` if it was deallocated externally
if (formattedTexts[i].GetLifeVersion() != formattedLifeVersions[i]) {
break;
}
return formattedTexts[i];
}
// `i` is equal to array index where `string` must be cached.
// Normally it's an index of the new element (`formattedTexts.length`),
// but can also contain already existing index if cached `Text`
// was invalidated.
result = _.text.FromFormattedString(string);
formattedStrings[i] = string;
formattedTexts[i] = result;
formattedLifeVersions[i] = result.GetLifeVersion();
return result;
}
public final function TextCache AddIndexedText(string string)
{
local Text newText;
indexedStrings[indexedStrings.length] = string;
newText = _.text.FromFormattedString(string);
indexedTexts[indexedTexts.length] = newText;
indexedLifeVersions[indexedLifeVersions.length] = newText.GetLifeVersion();
return self;
}
public final function Text GetIndexedText(int index)
{
local Text newText;
if (index < 0) return none;
if (index >= indexedTexts.length) return none;
if (indexedLifeVersions[index] == indexedTexts[index].GetLifeVersion()) {
return indexedTexts[index];
}
newText = __().text.FromFormattedString(indexedStrings[index]);
indexedLifeVersions[index] = newText.GetLifeVersion();
indexedTexts[index] = newText;
return newText;
}
defaultproperties
{
}

412
sources/Types/AcediaActor.uc

@ -0,0 +1,412 @@
/**
* Base actor class to be used in Acedia instead of an `Actor`.
* `AcediaActor` provides access to Acedia's APIs through an accessor to
* a `Global` object, built-in mechanism for storing unneeded references in
* an object pool and constructor/finalizer.
* It isn't guaranteed that `default._` will be defined for `AcediaActor`s.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AcediaActor extends Actor
abstract;
// Reference to Acedia's APIs for simple access.
var protected Global _;
// Object pool to store objects of a particular class
var private AcediaObjectPool _objectPool;
// Do we even use object pool?
var public const bool usesObjectPool;
// Is there a limit to it? Any negative number means unlimited pool size,
// `0` effectively disables object pool.
// This value can be changed through Acedia's system settings.
var public const int defaultMaxPoolSize;
// Same actor can be reallocated for different purposes and as far as
// users are concerned, - it should be considered a different actor after each
// reallocation.
// This variable stores a number unique to the current version and
// can help distinguish between them.
var private int _lifeVersion;
// Store allocation status to prevent possible issues
// (such as preventing finalizers or constructors being called several times)
// with freeing the same object several times without reallocating it
var private bool _isAllocated;
// Remembers (in it's `default` value) whether static constructor was already
// called for this object.
var private bool _staticConstructorWasCalled;
// We want to have a common `GetHashCode()` method for all Acedia's objects.
// Just randomizing one at the time of allocation seems like a great idea.
var private int _randomizedHashCode;
// This object will provide hashed `string` to `Text` map, necessary for
// efficient and convenient conversion methods.
// It is implemented as a separate object to facilitate static (per-class)
// hashing in `default` value that will not copy full stored stored data to
// every instance.
// We use a separate `TextCache` for every class, because that way
// efficiency of `string` to `Text` conversion depends only on amount of
// `string`s cached for a given class.
var private TextCache _textCache;
// Formatted `strings` declared in this array will be converted into `Text`s
// available via `T()` method when static constructor is called
// (either when first object of this class is created or
// `InitializeStatic()` method is called)
var protected const array<string> stringConstants;
/**
* FOR USE ONLY IN `MemoryAPI` METHODS.
*
* If object pool is enabled for this actor, - returns a reference to it.
* Time of object pool creation is undefined and can happen during this call.
*
* @return `AcediaObjectPool` that stores instances of caller actor's class,
* `none` iff `usesObjectPool == true || defaultMaxPoolSize == 0`.
*/
public final static function AcediaObjectPool _getPool()
{
local MemoryService service;
if (!default.usesObjectPool) {
return none;
}
if (default._objectPool == none) {
default._objectPool = new class'AcediaObjectPool';
default._objectPool.Initialize(default.class);
service = MemoryService(class'MemoryService'.static.Require());
if (service != none) {
service.RegisterNewPool(default._objectPool);
}
}
return default._objectPool;
}
/**
* This function is called upon caller actor allocation.
*
* Guaranteed to do nothing for allocated actors for which constructor
* was already called.
*
* AVOID MANUALLY CALLING IT, UNLESS YOU ARE REIMPLEMENTING `MemoryAPI`.
*/
public function _constructor()
{
if (_isAllocated) return;
_isAllocated = true;
_lifeVersion += 1;
_randomizedHashCode = Rand(MaxInt);
if (_ == none) {
default._ = class'Global'.static.GetInstance();
_ = default._;
}
Constructor();
}
/**
* This function is called upon caller actor deallocation.
*
* Guaranteed to do nothing for already deallocated actors.
*
* AVOID MANUALLY CALLING IT, UNLESS YOU ARE REIMPLEMENTING `MemoryAPI`.
*/
public function _finalizer()
{
if (!_isAllocated) return;
_isAllocated = false;
Finalizer();
}
/**
* Auxiliary method that helps child classes to decide whether calling static
* constructor is still needed.
*
* @return `true` if static constructor should not be called
* and `false` if it should.
*/
protected final static function bool StaticConstructorGuard()
{
if (!default._staticConstructorWasCalled)
{
default._staticConstructorWasCalled = true;
return false;
}
return true;
}
/**
* Method that can cause early static constructor call for a caller class.
*
* Normally static constructor is called the first time an instance of a class
* (or it's child class) is created, but this method can be used to cause this
* initialization early.
*/
public final static function InitializeStatic()
{
if (StaticConstructorGuard()) return;
StaticConstructor();
}
/**
* When using proper methods for creating actors (`MemoryAPI`),
* this method is guaranteed to be called after actor is spawned,
* but before it's returned from allocation method.
*
* AVOID MANUALLY CALLING IT, UNLESS YOU ARE REIMPLEMENTING `MemoryAPI`.
*/
protected function Constructor(){}
/**
* When using proper methods for creating objects (`MemoryAPI`),
* this method is guaranteed to be called after object of this (or it's child)
* class is deallocated.
*
* If you overload this method, first two lines must always be
* ____________________________________________________________________________
* | if (StaticConstructorGuard()) return;
* | super.StaticConstructor();
* |___________________________________________________________________________
* otherwise behavior of constructors should be considered undefined.
*/
protected static function StaticConstructor()
{
local int i;
local array<string> stringConstantsCopy;
if (StaticConstructorGuard()) return;
// If there is no string constants to convert into `Text`s,
// then this constructor has nothing to do.
if (default.stringConstants.length <= 0) return;
// Since `TextCache` does not have any string constants to convert,
// this check should never fail, but still do check to make extra sure
// there would not be any infinite loops if someone decided to add them.
if (default.class == class'TextCache') return;
if (default._textCache == none) {
default._textCache = TextCache(__().memory.Allocate(class'TextCache'));
}
// Create `Text` constants
stringConstantsCopy = default.stringConstants;
for (i = 0; i < stringConstantsCopy.length; i += 1) {
default._textCache.AddIndexedText(stringConstantsCopy[i]);
}
}
/**
* This method is called before actor is destroyed or deallocated by
* `MemoryAPI`.
*
* AVOID MANUALLY CALLING IT, UNLESS YOU ARE REIMPLEMENTING `MemoryAPI`.
*/
protected function Finalizer(){}
/**
* Acedia actors can be deallocated instead of being destroyed and
* deallocated instances should not be used while in the object pool.
* This method can be used to check if actor reference was deallocated.
*
* @return `true` if actor is allocated and ready to use, `false` otherwise.
*/
public final function bool IsAllocated()
{
return _isAllocated;
}
/**
* Marks caller `AcediaActor` free and stores it in the pool
* (if it is enabled and has free space).
*
* @param lifeVersion If specified, will only free actor that have provided
* life version. `<= 0` means actor must be freed regardless.
*/
public final function FreeSelf(optional int lifeVersion)
{
if (lifeVersion <= 0 || lifeVersion == GetLifeVersion()) {
_.memory.Free(self);
}
}
/**
* Determines whether passed `Object` is equal to the caller.
*
* By default simply compares references.
*
* Reimplementing `IsEqual()` is allowed, but you need to make sure that:
* 1. `a.IsEqual(b)` iff `b.IsEqual(a)`;
* 2. If `a.IsEqual(b)` then `a.GetHashCode() == b.GetHashCode()`.
*
* @param other Object to compare to the caller.
* `none` is only equal to the `none`.
* @return `true` if `other` is considered equal to the caller object,
* `false` otherwise.
*/
public function bool IsEqual(Object other)
{
return (self == other);
}
/**
* Returns hash of an object.
*
* If you overload `IsEqual()` method to allow two different objects to
* be equal, you must implement `GetHashCode()` to return the same hash
* for them.
*
* By default it is just a random value, generated at the time of allocation.
*
* @return Hash code for the caller actor.
*/
public function int GetHashCode()
{
return _randomizedHashCode;
}
/**
* Returns a positive number that uniquely changes for caller actor reference
* after each reallocation, which can help ensure that a reference was not
* deallocated and reallocated without us knowing at some point.
*
* If referred actor is not allocated at the moment, always returns `-1`
*
* @return A positive number unique for each reallocation of the caller's
* instance. `-1` if actor is not allocated.
*/
public final function int GetLifeVersion()
{
if (!IsAllocated()) {
return -1;
}
return _lifeVersion;
}
/**
* Method for returning predefined `Text` constants.
*
* You can define array `stringConstants` (of `string`s) in `defaultproperties`
* that will statically be converted into `Text` objects first time an object
* of that class is created or (`InitializeStatic()` method is called).
*
* `Text` instances
*/
public static final function Text T(int index)
{
if (default._textCache == none) {
default._textCache = TextCache(__().memory.Allocate(class'TextCache'));
}
return default._textCache.GetIndexedText(index);
}
/**
* Method for creating `Text` objects from `string` variables.
*
* Difference with `_.text.FromString()` method is that it will return
* the same `Text` instance for the same passed `string`, removing the need to
* deallocate created object.
* Exception is when returned `Text` instance is deallocated, then this
* method will allocate a new `Text` object.
*
* @param string Plain `string` data to copy into a returned `Text` instance.
* @return `Text` instance that contains data from plain `string`.
* Guaranteed to be allocated, not `none`.
*/
public static final function Text P(string string)
{
if (default._textCache == none) {
default._textCache = TextCache(__().memory.Allocate(class'TextCache'));
}
return default._textCache.GetPlainText(string);
}
/**
* Method for creating `Text` objects from `string` variables.
*
* Difference with `_.text.FromString()` method is that it will return
* the same `Text` instance for the same passed `string`, removing the need to
* deallocate created object.
* Exception is when returned `Text` instance is deallocated, then this
* method will allocate a new `Text` object.
*
* @param string Colored `string` data to copy into a returned
* `Text` instance.
* @return `Text` instance that contains data from colored `string`.
* Guaranteed to be allocated, not `none`.
*/
public static final function Text C(string string)
{
if (default._textCache == none) {
default._textCache = TextCache(__().memory.Allocate(class'TextCache'));
}
return default._textCache.GetColoredText(string);
}
/**
* Method for creating `Text` objects from `string` variables.
*
* Difference with `_.text.FromString()` method is that it will return
* the same `Text` instance for the same passed `string`, removing the need to
* deallocate created object.
* Exception is when returned `Text` instance is deallocated, then this
* method will allocate a new `Text` object.
*
* @param string Formatted `string` data to copy into a returned
* `Text` instance.
* @return `Text` instance that contains data from formatted `string`.
* Guaranteed to be allocated, not `none`.
*/
public static final function Text F(string string)
{
if (default._textCache == none) {
default._textCache = TextCache(__().memory.Allocate(class'TextCache'));
}
return default._textCache.GetFormattedText(string);
}
/**
* Static method accessor to API namespace, necessary for Acedia's
* implementation.
*/
public static final function Global __()
{
return class'Global'.static.GetInstance();
}
/**
* If you are overloading this event - you have to first call
* `super.PreBeginPlay()`, otherwise Acedia might not function properly.
*/
event PreBeginPlay()
{
super.PreBeginPlay();
_constructor();
}
/**
* If you are overloading this event - you have to call `super.Destroyed()`,
* otherwise Acedia might not function properly.
*/
event Destroyed()
{
super.Destroyed();
_finalizer();
}
defaultproperties
{
usesObjectPool = false
defaultMaxPoolSize = 0
}

418
sources/Types/AcediaObject.uc

@ -0,0 +1,418 @@
/**
* Base object class to be used in Acedia instead of an `Object`.
* `AcediaObject` provides access to Acedia's APIs through an accessor to
* a `Global` object, built-in mechanism for storing unneeded references in
* an object pool and constructor/finalizer.
* Since `Global` is an actor, we wish to avoid storing it's instance in
* the object because it can mess with garbage collection on level change.
* So we provide an accessor function `_` instead.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AcediaObject extends Object
abstract;
// Reference to Acedia's APIs for simple access.
var protected Global _;
// Object pool to store objects of a particular class
var private AcediaObjectPool _objectPool;
// Do we even use object pool?
var public const bool usesObjectPool;
// Is there a limit to it? Any negative number means unlimited pool size,
// `0` effectively disables object pool.
// This value can be changes through Acedia's system settings.
var public const int defaultMaxPoolSize;
// Same object can be reallocated for different purposes and as far as
// users are concerned, - it should be considered a different object after each
// reallocation.
// This variable stores a number unique to the current version and
// can help distinguish between them.
var private int _lifeVersion;
// Store allocation status to prevent possible issues
// (such as preventing finalizers or constructors being called several times)
// with freeing the same object several times without reallocating it
var private bool _isAllocated;
// Remembers (in it's `default` value) whether static constructor was already
// called for this object.
var private bool _staticConstructorWasCalled;
// We want to have a common `GetHashCode()` method for all Acedia's objects.
// Just randomizing one at the time of allocation seems like a great idea.
var private int _randomizedHashCode;
// This object will provide hashed `string` to `Text` map, necessary for
// efficient and convenient conversion methods.
// It is implemented as a separate object to facilitate static (per-class)
// hashing in `default` value that will not copy full stored stored data to
// every instance.
// We use a separate `TextCache` for every class, because that way
// efficiency of `string` to `Text` conversion depends only on amount of
// `string`s cached for a given class.
var private TextCache _textCache;
// Formatted `strings` declared in this array will be converted into `Text`s
// available via `T()` method when static constructor is called
// (either when first object of this class is created or
// `InitializeStatic()` method is called)
var protected const array<string> stringConstants;
/**
* FOR USE ONLY IN `MemoryAPI` METHODS.
*
* If object pool is enabled for this object, - returns a reference to it.
* Time of object pool creation is undefined and can happen during this call.
*
* @return `AcediaObjectPool` that stores instances of caller object's class,
* `none` iff `usesObjectPool == true || defaultMaxPoolSize == 0`.
*/
public final static function AcediaObjectPool _getPool()
{
local MemoryService service;
if (!default.usesObjectPool) {
return none;
}
if (default._objectPool == none) {
default._objectPool = new class'AcediaObjectPool';
default._objectPool.Initialize(default.class);
service = MemoryService(class'MemoryService'.static.Require());
if (service != none) {
service.RegisterNewPool(default._objectPool);
}
}
return default._objectPool;
}
/**
* This function is called upon caller object allocation.
*
* Guaranteed to do nothing for allocated object, for which constructor
* was already called.
*
* AVOID MANUALLY CALLING IT, UNLESS YOU ARE REIMPLEMENTING `MemoryAPI`.
*/
public final function _constructor()
{
if (_isAllocated) return;
_isAllocated = true;
_lifeVersion += 1;
_randomizedHashCode = Rand(MaxInt);
if (_ == none) {
default._ = class'Global'.static.GetInstance();
_ = default._;
}
if (!default._staticConstructorWasCalled) {
StaticConstructor();
default._staticConstructorWasCalled = true;
}
Constructor();
}
/**
* This function is called upon caller object deallocation.
*
* Guaranteed to do nothing for already deallocated objects.
*
* AVOID MANUALLY CALLING IT, UNLESS YOU ARE REIMPLEMENTING `MemoryAPI`.
*/
public final function _finalizer()
{
if (!_isAllocated) return;
_isAllocated = false;
Finalizer();
}
/**
* Auxiliary method that helps child classes to decide whether calling static
* constructor is still needed.
*
* @return `true` if static constructor should not be called
* and `false` if it should.
*/
protected final static function bool StaticConstructorGuard()
{
if (!default._staticConstructorWasCalled)
{
default._staticConstructorWasCalled = true;
return false;
}
return true;
}
/**
* Method that can cause early static constructor call for a caller class.
*
* Normally static constructor is called the first time an instance of a class
* (or it's child class) is created, but this method can be used to cause this
* initialization early.
*/
public final static function InitializeStatic()
{
if (default._staticConstructorWasCalled) return;
StaticConstructor();
}
/**
* When using proper methods for creating objects (`MemoryAPI`),
* this method is guaranteed to be called after object is allocated,
* but before it's returned from allocation method.
*
* AVOID MANUALLY CALLING IT, UNLESS YOU ARE REIMPLEMENTING `MemoryAPI`.
*/
protected function Constructor(){}
/**
* When using proper methods for creating objects (`MemoryAPI`),
* this method is guaranteed to be called after object of this (or it's child)
* class is deallocated.
*
* If you overload this method, first two lines must always be
* ____________________________________________________________________________
* | if (StaticConstructorGuard()) return;
* | super.StaticConstructor();
* |___________________________________________________________________________
* otherwise behavior of constructors should be considered undefined.
*/
protected static function StaticConstructor()
{
local int i;
local array<string> stringConstantsCopy;
if (StaticConstructorGuard()) return;
// If there is no string constants to convert into `Text`s,
// then this constructor has nothing to do.
if (default.stringConstants.length <= 0) return;
// Since `TextCache` does not have any string constants to convert,
// this check should never fail, but still do check to make extra sure
// there would not be any infinite loops if someone decided to add them.
if (default.class == class'TextCache') return;
if (default._textCache == none) {
default._textCache = TextCache(__().memory.Allocate(class'TextCache'));
}
// Create `Text` constants
stringConstantsCopy = default.stringConstants;
for (i = 0; i < stringConstantsCopy.length; i += 1) {
default._textCache.AddIndexedText(stringConstantsCopy[i]);
}
}
/**
* When using proper methods for creating objects (`MemoryAPI`),
* this method is guaranteed to be called after object is deallocated.
*
* AVOID MANUALLY CALLING IT, UNLESS YOU ARE REIMPLEMENTING `MemoryAPI`.
*/
protected function Finalizer(){}
/**
* Acedia objects can be deallocated into an object pool to be reused later and
* such instances should not be used while in the pool.
* This method can be used to check if object reference was deallocated.
*
* @return `true` if object is allocated and ready to use, `false` otherwise.
*/
public final function bool IsAllocated()
{
return _isAllocated;
}
/**
* Marks caller `AcediaObject` free and stores it in the pool
* (if it is enabled and has free space).
*
* @param lifeVersion If specified, will only free object that have provided
* life version. `<= 0` means object must be freed regardless.
*/
public final function FreeSelf(optional int lifeVersion)
{
if (lifeVersion <= 0 || lifeVersion == GetLifeVersion()) {
_.memory.Free(self);
}
}
/**
* Determines whether passed `Object` is equal to the caller.
*
* By default simply compares references.
*
* Reimplementing `IsEqual()` is allowed, but you need to make sure that:
* 1. `a.IsEqual(b)` iff `b.IsEqual(a)`;
* 2. If `a.IsEqual(b)` then `a.GetHashCode() == b.GetHashCode()`.
*
* @param other Object to compare to the caller.
* `none` is only equal to the `none`.
* @return `true` if `other` is considered equal to the caller object,
* `false` otherwise.
*/
public function bool IsEqual(Object other)
{
return (self == other);
}
/**
* Returns hash of an object.
*
* If you overload `IsEqual()` method to allow two different objects to
* be equal, you must implement `GetHashCode()` to return the same hash
* for them.
*
* By default it is just a random value, generated at the time of allocation.
*
* @return Hash code for the caller object.
*/
public function int GetHashCode()
{
return _randomizedHashCode;
}
/**
* Auxiliary method for combining different numeric values into a single hash.
*
* @param accumulator Hash generated so far, from other values.
* @param otherValue Other value to base a hash on.
* @return Hash, calculated so far, can be further combined
* with `CombineHash()`.
*/
protected function int CombineHash(int accumulator, int nextValue)
{
// accumulator * 33 + nextValue
return ((accumulator << 5) + accumulator) + nextValue;
}
/**
* Returns a positive number that uniquely changes for caller object reference
* after each reallocation, which can help ensure that a reference was not
* deallocated and reallocated without us knowing at some point.
*
* If referred object is not allocated at the moment, always returns `-1`
*
* @return A positive number unique for each reallocation of the caller's
* instance. `-1` if object is not allocated.
*/
public final function int GetLifeVersion()
{
if (!IsAllocated()) {
return -1;
}
return _lifeVersion;
}
/**
* Method for returning predefined `Text` constants.
*
* You can define array `stringConstants` (of `string`s) in `defaultproperties`
* that will statically be converted into `Text` objects first time an object
* of that class is created or (`InitializeStatic()` method is called).
*
* Provided that returned values are not deallocated, they always refer to
* the same `Text` object for any fixed `index`.
* Otherwise new `Text` object can be allocated.
*
* @param index Index for which to return `Text` instance.
* @return `Text` instance containing the data in a `stringConstants[index]`.
* `none` if either `index < 0` or `index >= stringConstants.length`,
* otherwise guaranteed to be not `none`.
*/
public static final function Text T(int index)
{
if (default._textCache == none) {
default._textCache = TextCache(__().memory.Allocate(class'TextCache'));
}
return default._textCache.GetIndexedText(index);
}
/**
* Method for creating `Text` objects from `string` variables.
*
* Difference with `_.text.FromString()` method is that it will return
* the same `Text` instance for the same passed `string`, removing the need to
* deallocate created object.
* Exception is when returned `Text` instance is deallocated, then this
* method will allocate a new `Text` object.
*
* @param string Plain `string` data to copy into a returned `Text` instance.
* @return `Text` instance that contains data from plain `string`.
* Guaranteed to be allocated, not `none`.
*/
public static final function Text P(string string)
{
if (default._textCache == none) {
default._textCache = TextCache(__().memory.Allocate(class'TextCache'));
}
return default._textCache.GetPlainText(string);
}
/**
* Method for creating `Text` objects from `string` variables.
*
* Difference with `_.text.FromString()` method is that it will return
* the same `Text` instance for the same passed `string`, removing the need to
* deallocate created object.
* Exception is when returned `Text` instance is deallocated, then this
* method will allocate a new `Text` object.
*
* @param string Colored `string` data to copy into a returned
* `Text` instance.
* @return `Text` instance that contains data from colored `string`.
* Guaranteed to be allocated, not `none`.
*/
public static final function Text C(string string)
{
if (default._textCache == none) {
default._textCache = TextCache(__().memory.Allocate(class'TextCache'));
}
return default._textCache.GetColoredText(string);
}
/**
* Method for creating `Text` objects from `string` variables.
*
* Difference with `_.text.FromString()` method is that it will return
* the same `Text` instance for the same passed `string`, removing the need to
* deallocate created object.
* Exception is when returned `Text` instance is deallocated, then this
* method will allocate a new `Text` object.
*
* @param string Formatted `string` data to copy into a returned
* `Text` instance.
* @return `Text` instance that contains data from formatted `string`.
* Guaranteed to be allocated, not `none`.
*/
public static final function Text F(string string)
{
if (default._textCache == none) {
default._textCache = TextCache(__().memory.Allocate(class'TextCache'));
}
return default._textCache.GetFormattedText(string);
}
/**
* Static method accessor to API namespace, necessary for Acedia's
* implementation.
*/
public static final function Global __()
{
return class'Global'.static.GetInstance();
}
defaultproperties
{
usesObjectPool = true
defaultMaxPoolSize = -1
}

21
sources/AcediaObject.uc → sources/Types/Boxes/ArrayBox.uc

@ -1,10 +1,7 @@
/**
* Object base class to be used to Acedia instead of an `Object`.
* The only difference is defined `_` member that provides convenient access to
* Acedia's API.
* Since `Global` is an actor, we wish to avoid storing it's instance in
* the object because it can mess with garbage collection on level change.
* So we provide an accessor function `_()` instead.
* This file either is or was auto-generated from the template for
* array references.
* Boxes are immutable wrappers around primitive values and arrays.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
@ -22,19 +19,9 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AcediaObject extends Object
class ArrayBox extends ValueBox
abstract;
public final function Text T(string string)
{
return _().text.FromString(string);
}
public static final function Global _()
{
return Global(class'Global'.static.GetInstance());
}
defaultproperties
{
}

141
sources/Types/Boxes/BoxAPI.uc

@ -0,0 +1,141 @@
/**
* Convenience API that provides methods for quickly creating
* box objects for native types.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class BoxAPI extends AcediaObject;
/**
* Creates initialized box that stores a `bool` value.
*
* @param value Value to store in the box.
* @return `BoolBox`, containing `value`.
*/
public final function BoolBox Bool(optional bool value)
{
local BoolBox box;
box = BoolBox(_.memory.Allocate(class'BoolBox'));
box.Initialize(value);
return box;
}
/**
* Creates initialized box that stores an array of `bool` values.
* Initializes it with a given array.
*
* @param arrayValue Initial array value to store in the box.
* @return `BoolArrayBox`, containing `arrayValue`.
*/
public final function BoolArrayBox BoolArray(array<bool> arrayValue)
{
local BoolArrayBox box;
box = BoolArrayBox(_.memory.Allocate(class'BoolArrayBox'));
box.Initialize(arrayValue);
return box;
}
/**
* Creates initialized box that stores a `byte` value.
*
* @param value Value to store in the box.
* @return `ByteBox`, containing `value`.
*/
public final function ByteBox Byte(optional byte value)
{
local ByteBox box;
box = ByteBox(_.memory.Allocate(class'ByteBox'));
box.Initialize(value);
return box;
}
/**
* Creates initialized box that stores an array of `byte` values.
* Initializes it with a given array.
*
* @param arrayValue Initial array value to store in the box.
* @return `ByteArrayBox`, containing `arrayValue`.
*/
public final function ByteArrayBox ByteArray(array<byte> arrayValue)
{
local ByteArrayBox box;
box = ByteArrayBox(_.memory.Allocate(class'ByteArrayBox'));
box.Initialize(arrayValue);
return box;
}
/**
* Creates initialized box that stores a `float` value.
*
* @param value Value to store in the box.
* @return `FloatBox`, containing `value`.
*/
public final function FloatBox Float(optional float value)
{
local FloatBox box;
box = FloatBox(_.memory.Allocate(class'FloatBox'));
box.Initialize(value);
return box;
}
/**
* Creates initialized box that stores an array of `float` values.
* Initializes it with a given array.
*
* @param arrayValue Initial array value to store in the box.
* @return `FloatArrayBox`, containing `arrayValue`.
*/
public final function FloatArrayBox FloatArray(array<float> arrayValue)
{
local FloatArrayBox box;
box = FloatArrayBox(_.memory.Allocate(class'FloatArrayBox'));
box.Initialize(arrayValue);
return box;
}
/**
* Creates initialized box that stores an `int` value.
*
* @param value Value to store in the box.
* @return `IntBox`, containing `value`.
*/
public final function IntBox Int(optional int value)
{
local IntBox box;
box = IntBox(_.memory.Allocate(class'IntBox'));
box.Initialize(value);
return box;
}
/**
* Creates initialized box that stores an array of `int` values.
* Initializes it with a given array.
*
* @param arrayValue Initial array value to store in the box.
* @return `IntArrayBox`, containing `arrayValue`.
*/
public final function IntArrayBox IntArray(array<int> arrayValue)
{
local IntArrayBox box;
box = IntArrayBox(_.memory.Allocate(class'IntArrayBox'));
box.Initialize(arrayValue);
return box;
}
defaultproperties
{
}

BIN
sources/Types/Boxes/Native/BoolArrayBox.uc

Binary file not shown.

BIN
sources/Types/Boxes/Native/BoolBox.uc

Binary file not shown.

BIN
sources/Types/Boxes/Native/ByteArrayBox.uc

Binary file not shown.

BIN
sources/Types/Boxes/Native/ByteBox.uc

Binary file not shown.

BIN
sources/Types/Boxes/Native/FloatArrayBox.uc

Binary file not shown.

BIN
sources/Types/Boxes/Native/FloatBox.uc

Binary file not shown.

BIN
sources/Types/Boxes/Native/IntArrayBox.uc

Binary file not shown.

BIN
sources/Types/Boxes/Native/IntBox.uc

Binary file not shown.

BIN
sources/Types/Boxes/Native/StringArrayBox.uc

Binary file not shown.

BIN
sources/Types/Boxes/Native/StringBox.uc

Binary file not shown.

56
sources/Types/Boxes/ValueBox.uc

@ -0,0 +1,56 @@
/**
* This file either is or was auto-generated from the template for
* value box.
* Boxes are immutable wrappers around primitive values and arrays.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ValueBox extends AcediaObject
abstract;
var private bool boxInitialized;
/**
* Marks caller box as initialized with value.
*/
protected final function MarkInitialized()
{
boxInitialized = true;
}
/**
* Checks if caller box was initialized.
* Once initialized box cannot be de-initialized without deallocation.
*/
public final function bool IsInitialized()
{
return boxInitialized;
}
protected function Constructor()
{
boxInitialized = false;
}
protected function Finalizer()
{
boxInitialized = false;
}
defaultproperties
{
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save