diff --git a/config/AcediaAliases.ini b/config/AcediaAliases.ini new file mode 100644 index 0000000..e69de29 diff --git a/config/AcediaAliases_Colors.ini b/config/AcediaAliases_Colors.ini index 0cb8609..1590dfa 100644 --- a/config/AcediaAliases_Colors.ini +++ b/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)") diff --git a/config/AcediaSystem.ini b/config/AcediaSystem.ini index a683545..41b996f 100644 --- a/config/AcediaSystem.ini +++ b/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=,maxPoolSize=) + [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 diff --git a/docs/API/Collections.md b/docs/API/Collections.md new file mode 100644 index 0000000..c1e8bc7 --- /dev/null +++ b/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` argument and populates returned `DynamicArray` with it's items, while `_.collections.EmptyDynamicArray()` simply creates an empty `DynamicArray`. + +They are similar to regular dynamic `array`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`, `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. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..f1a8f81 --- /dev/null +++ b/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) diff --git a/docs/introduction/ObjectsActors.md b/docs/introduction/ObjectsActors.md new file mode 100644 index 0000000..ab1b67f --- /dev/null +++ b/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`. + +Following code illustrates it: + +```unrealscript +local IntBox numberBox; +local BoolBox logicBox; +local array 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()`. diff --git a/docs/introduction/Text.md b/docs/introduction/Text.md new file mode 100644 index 0000000..2816d38 --- /dev/null +++ b/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>` 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). diff --git a/sources/AcediaObjectPool.uc b/sources/AcediaObjectPool.uc new file mode 100644 index 0000000..43c0480 --- /dev/null +++ b/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 . + */ +class AcediaObjectPool extends Object + config(AcediaSystem); + +// Class of objects that this `AcediaObjectPool` stores. +// if `== none`, - object pool is considered uninitialized. +var private class storedClass; +// Actual storage, functions on LIFO principle. +var private array objectPool; + +// This struct and it's associated array `poolSizeOverwrite` allows +// server admins to rewrite the pool capacity for each class. +struct PoolSizeSetting +{ + var class objectClass; + var int maxPoolSize; +}; +var private config const array 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 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 classToCheck) +{ + local int i; + local int result; + local class classAsAcediaObject; + local class classAsAcediaActor; + // Get hard-coded value + classAsAcediaObject = class(classToCheck); + classAsAcediaActor = class(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 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(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 +{ +} \ No newline at end of file diff --git a/sources/Aliases/AliasHash.uc b/sources/Aliases/AliasHash.uc deleted file mode 100644 index 43da820..0000000 --- a/sources/Aliases/AliasHash.uc +++ /dev/null @@ -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 . - */ -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 pairs; -}; -var private array 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 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 -} \ No newline at end of file diff --git a/sources/Aliases/AliasSource.uc b/sources/Aliases/AliasSource.uc index 6f863ea..9f06378 100644 --- a/sources/Aliases/AliasSource.uc +++ b/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 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 objectNames; - entriesAmount = record.length; - if (aliasesClass == none) { - return entriesAmount; + local int i; + local Text aliasAsText, valueAsText; + for (i = 0; i < record.length; i += 1) + { + aliasAsText = _.text.FromString(record[i].alias); + valueAsText = _.text.FromString(record[i].value); + InsertAlias(aliasAsText, valueAsText); + aliasAsText.FreeSelf(); + valueAsText.FreeSelf(); } - objectNames = - GetPerObjectNames(configName, string(aliasesClass.name), MaxInt); - loadedAliasObjects.length = objectNames.length; - for (i = 0; i < objectNames.length; i += 1) +} + +// Load hashes from `Aliases` objects' config +private final function HashValidAliasesFromPerObjectConfig() +{ + local int i, j; + local Text nextValue; + local array valueAliases; + for (i = 0; i < loadedAliasObjects.length; i += 1) { - loadedAliasObjects[i] = new(none, objectNames[i]) aliasesClass; - entriesAmount += loadedAliasObjects[i].GetAliases().length; + 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); } - 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) +// 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) { - return hash.Contains(alias); + 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. - * - * Also see `Try()` method. + * Checks if given alias is present in caller `AliasSource`. * - * @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; + } + lowerCaseAlias = alias.LowerCopy(); + result = Text(aliasHash.GetItem(lowerCaseAlias)); + lowerCaseAlias.FreeSelf(); + if (result != none) { + return result.Copy(); + } + if (copyOnFailure) { + return alias.Copy(); } - return alias; + 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 AliasValuePair newPair; - if (_.alias.IsAliasValid(aliasToAdd)) { - return false; - } - if (hash.Contains(aliasToAdd)) { + local Text lowerCaseAlias; + local AliasValuePair newPair; + 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 removedAliasFromRecord; - hash.Remove(aliasToRemove); + local int i; + local bool isMatchingRecord; + local bool removedAliasFromRecord; + 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 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 + aliasesClass = class'Aliases' } \ No newline at end of file diff --git a/sources/Aliases/Aliases.uc b/sources/Aliases/Aliases.uc index 8b30683..c59ea57 100644 --- a/sources/Aliases/Aliases.uc +++ b/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 sourceClass; @@ -38,17 +42,68 @@ var public const class sourceClass; // defined by this object's name `string(self.name)`. var protected config array 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.ToPlainString(), ".", ":"); +} + +// See comment to `ToStorageVersion()`. +private final static function Text ToActualVersion(string 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 LoadAllObjects() { - return Repl(actualValue, ".", ":"); + local int i; + local array objectNames; + local array 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; } -private final function string ToActualVersion(string storageValue) +// Loads a new `Aliases` object by it's given name (`objectName`). +private static final function Aliases LoadObjectByName(string objectName) { - return Repl(storageValue, ":", "."); + 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 GetAliases() +public final function array GetAliases() { - return alias; + local int i; + local array 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" } \ No newline at end of file diff --git a/sources/Aliases/AliasesAPI.uc b/sources/Aliases/AliasesAPI.uc index d231640..84250b7 100644 --- a/sources/Aliases/AliasesAPI.uc +++ b/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 . */ -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 source.Resolve(alias, copyOnFailure); } - return false; + return none; } /** - * 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; -} - -/** - * 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 diff --git a/sources/Aliases/BuiltInSources/ColorAliasSource.uc b/sources/Aliases/BuiltInSources/ColorAliasSource.uc index 5cd75cf..73edfea 100644 --- a/sources/Aliases/BuiltInSources/ColorAliasSource.uc +++ b/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' } \ No newline at end of file diff --git a/sources/Aliases/BuiltInSources/ColorAliases.uc b/sources/Aliases/BuiltInSources/ColorAliases.uc index d0998b6..9a00b78 100644 --- a/sources/Aliases/BuiltInSources/ColorAliases.uc +++ b/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' } \ No newline at end of file diff --git a/sources/Aliases/BuiltInSources/WeaponAliasSource.uc b/sources/Aliases/BuiltInSources/WeaponAliasSource.uc index 0cf1bc4..42632f1 100644 --- a/sources/Aliases/BuiltInSources/WeaponAliasSource.uc +++ b/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' } \ No newline at end of file diff --git a/sources/Aliases/BuiltInSources/WeaponAliases.uc b/sources/Aliases/BuiltInSources/WeaponAliases.uc index 82acd45..03f84d3 100644 --- a/sources/Aliases/BuiltInSources/WeaponAliases.uc +++ b/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' } \ No newline at end of file diff --git a/sources/Aliases/Tests/MockAliasSource.uc b/sources/Aliases/Tests/MockAliasSource.uc index d724501..cc57711 100644 --- a/sources/Aliases/Tests/MockAliasSource.uc +++ b/sources/Aliases/Tests/MockAliasSource.uc @@ -22,6 +22,5 @@ class MockAliasSource extends AliasSource defaultproperties { - configName = "AcediaAliases_Tests" aliasesClass = class'MockAliases' } \ No newline at end of file diff --git a/sources/Aliases/Tests/MockAliases.uc b/sources/Aliases/Tests/MockAliases.uc index 83eeef3..dcc73e6 100644 --- a/sources/Aliases/Tests/MockAliases.uc +++ b/sources/Aliases/Tests/MockAliases.uc @@ -23,5 +23,6 @@ class MockAliases extends Aliases defaultproperties { + configName = "AcediaAliases_Tests" sourceClass = class'MockAliasSource' } \ No newline at end of file diff --git a/sources/Aliases/Tests/TEST_Aliases.uc b/sources/Aliases/Tests/TEST_Aliases.uc index be51505..b15329c 100644 --- a/sources/Aliases/Tests/TEST_Aliases.uc +++ b/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(); @@ -35,99 +29,51 @@ 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," + local AliasSource source; + 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`."); + local AliasSource source; 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" } \ No newline at end of file diff --git a/sources/Color/ColorAPI.uc b/sources/Color/ColorAPI.uc index e292c16..56bda53 100644 --- a/sources/Color/ColorAPI.uc +++ b/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 . */ -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) + string stringWithColor, + 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 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) diff --git a/sources/Color/Tests/TEST_ColorAPI.uc b/sources/Color/Tests/TEST_ColorAPI.uc index d8c2d72..d39df04 100644 --- a/sources/Color/Tests/TEST_ColorAPI.uc +++ b/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) - ~= "rgb(r=0,g=255,b=255)"); + 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" } \ No newline at end of file diff --git a/sources/Commands/Aliases/CommandAliasSource.uc b/sources/Commands/Aliases/CommandAliasSource.uc new file mode 100644 index 0000000..745452f --- /dev/null +++ b/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 . + */ +class CommandAliasSource extends AliasSource; + +defaultproperties +{ + aliasesClass = class'CommandAliases' +} \ No newline at end of file diff --git a/sources/Commands/Aliases/CommandAliases.uc b/sources/Commands/Aliases/CommandAliases.uc new file mode 100644 index 0000000..3faaf93 --- /dev/null +++ b/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 . + */ +class CommandAliases extends Aliases + perObjectConfig; + +defaultproperties +{ + sourceClass = class'CommandAliasSource' +} \ No newline at end of file diff --git a/sources/Commands/BroadcastListener_Commands.uc b/sources/Commands/BroadcastListener_Commands.uc new file mode 100644 index 0000000..24b95bc --- /dev/null +++ b/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 . + */ +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 +{ +} \ No newline at end of file diff --git a/sources/Commands/BuiltInCommands/ACommandDosh.uc b/sources/Commands/BuiltInCommands/ACommandDosh.uc new file mode 100644 index 0000000..eebacec --- /dev/null +++ b/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 . + */ +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 " + @ "of money.")); + builder.SubCommand(P("set")) + .ParamIntegerList(P("amount")) + .Describe(P("Sets players' money to a specified .")); + 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" +} \ No newline at end of file diff --git a/sources/Commands/BuiltInCommands/ACommandHelp.uc b/sources/Commands/BuiltInCommands/ACommandHelp.uc new file mode 100644 index 0000000..8fd29e9 --- /dev/null +++ b/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 . + */ +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 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" +} \ No newline at end of file diff --git a/sources/Commands/BuiltInCommands/ACommandNick.uc b/sources/Commands/BuiltInCommands/ACommandNick.uc new file mode 100644 index 0000000..d75b817 --- /dev/null +++ b/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 . + */ +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" +} \ No newline at end of file diff --git a/sources/Commands/Command.uc b/sources/Commands/Command.uc new file mode 100644 index 0000000..f3d3ecf --- /dev/null +++ b/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 . + */ +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 +// " "). +// 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 required; + var array 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 required; + var array optional; +}; + +// Structure that defines what sub-commands and options command has +// (and what parameters they take) +struct Data +{ + var protected array subCommands; + var protected array