diff --git a/sources/Core/Aliases/AliasHash.uc b/sources/Core/Aliases/AliasHash.uc
new file mode 100644
index 0000000..43da820
--- /dev/null
+++ b/sources/Core/Aliases/AliasHash.uc
@@ -0,0 +1,218 @@
+/**
+ * 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/Core/Aliases/AliasService.uc b/sources/Core/Aliases/AliasService.uc
new file mode 100644
index 0000000..ac9cd94
--- /dev/null
+++ b/sources/Core/Aliases/AliasService.uc
@@ -0,0 +1,135 @@
+/**
+ * Service that handles pending saving of aliases data into configs.
+ * Adding aliases into `AliasSource`s causes corresponding configs to update.
+ * This service allows to delay and spread config rewrites over time,
+ * which should help in case someone dynamically adds a lot of
+ * different aliases.
+ * 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 AliasService extends Service
+ config(AcediaSystem);
+
+// Objects for which we are yet to write configs
+var private array sourcesPendingToSave;
+var private array aliasesPendingToSave;
+// How often should we do it.
+// Negative or zero values would be reset to `0.05`.
+var public config const float saveInterval;
+
+// To avoid creating yet another object for aliases system we will
+// keep config variable pointing to weapon, color, etc. `AliasSource`
+// subclasses here. It's not the best regarding separation of responsibility,
+// but should make config files less fragmented.
+// Changing these allows you to change in what sources `AliasesAPI`
+// looks for weapon and color aliases.
+var public config const class weaponAliasesSource;
+var public config const class colorAliasesSource;
+
+protected function OnLaunch()
+{
+ local float actualInterval;
+ actualInterval = saveInterval;
+ if (actualInterval <= 0)
+ {
+ actualInterval = 0.05;
+ }
+ SetTimer(actualInterval, true);
+}
+
+protected function OnShutdown()
+{
+ SaveAllPendingObjects();
+}
+
+public final function PendingSaveSource(AliasSource sourceToSave)
+{
+ local int i;
+ if (sourceToSave == none) return;
+ // Starting searching from the end of an array will make situations when
+ // we add several aliases to a single source in a row more efficient.
+ for (i = sourcesPendingToSave.length - 1;i >= 0; i -= 1) {
+ if (sourcesPendingToSave[i] == sourceToSave) return;
+ }
+ sourcesPendingToSave[sourcesPendingToSave.length] = sourceToSave;
+}
+
+public final function PendingSaveObject(Aliases objectToSave)
+{
+ local int i;
+ if (objectToSave == none) return;
+ // Starting searching from the end of an array will make situations when
+ // we add several aliases to a single `Aliases` object in a row
+ // more efficient.
+ for (i = aliasesPendingToSave.length - 1;i >= 0; i -= 1) {
+ if (aliasesPendingToSave[i] == objectToSave) return;
+ }
+ aliasesPendingToSave[aliasesPendingToSave.length] = objectToSave;
+}
+
+/**
+ * Forces saving of the next object (either `AliasSource` or `Aliases`)
+ * in queue to the config file.
+ *
+ * Does not reset the timer until next saving.
+ */
+private final function DoSaveNextPendingObject()
+{
+ if (sourcesPendingToSave.length > 0)
+ {
+ if (sourcesPendingToSave[0] != none) {
+ sourcesPendingToSave[0].SaveConfig();
+ }
+ sourcesPendingToSave.Remove(0, 1);
+ return;
+ }
+ if (aliasesPendingToSave.length > 0)
+ {
+ aliasesPendingToSave[0].SaveOrClear();
+ aliasesPendingToSave.Remove(0, 1);
+ }
+}
+
+/**
+ * Forces saving of all objects (both `AliasSource`s or `Aliases`s) in queue
+ * to their config files.
+ */
+private final function SaveAllPendingObjects()
+{
+ local int i;
+ for (i = 0; i < sourcesPendingToSave.length; i += 1) {
+ if (sourcesPendingToSave[i] == none) continue;
+ sourcesPendingToSave[i].SaveConfig();
+ }
+ for (i = 0; i < aliasesPendingToSave.length; i += 1) {
+ aliasesPendingToSave[i].SaveOrClear();
+ }
+ sourcesPendingToSave.length = 0;
+ aliasesPendingToSave.length = 0;
+}
+
+event Timer()
+{
+ DoSaveNextPendingObject();
+}
+
+defaultproperties
+{
+ saveInterval = 0.05
+ weaponAliasesSource = class'WeaponAliasSource'
+ colorAliasesSource = class'ColorAliasSource'
+}
\ No newline at end of file
diff --git a/sources/Core/Aliases/AliasSource.uc b/sources/Core/Aliases/AliasSource.uc
new file mode 100644
index 0000000..6f863ea
--- /dev/null
+++ b/sources/Core/Aliases/AliasSource.uc
@@ -0,0 +1,379 @@
+/**
+ * Aliases allow users to define human-readable and easier to use
+ * "synonyms" to some symbol sequences (mainly names of UnrealScript classes).
+ * This class implements an alias database that stores aliases inside
+ * 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
+ *------------------------------------------------------------------------------
+ * 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 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
+// only store aliases in form of `record=(alias="...",value="...")`.
+var public const class aliasesClass;
+// Storage for all objects of `aliasesClass` class in the config.
+// Exists after `OnCreated()` event and is maintained up-to-date at all times.
+var private array loadedAliasObjects;
+
+// Links alias to a value.
+// An array of these structures (without duplicate `alias` records) defines
+// a function from the space of aliases to the space of values.
+struct AliasValuePair
+{
+ var string alias;
+ var string value;
+};
+// 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.
+// 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;
+
+// Load and hash all the data `AliasSource` creation.
+protected function OnCreated()
+{
+ local int entriesAmount;
+ if (!AssertAliasesClassIsOwnedByMe()) {
+ return;
+ }
+ // Load and hash
+ entriesAmount = LoadData();
+ hash = AliasHash(_.memory.Allocate(class'AliasHash'));
+ hash.Initialize(int(entriesAmount * HASH_TABLE_SCALE));
+ HashValidAliases();
+}
+
+// Ensures invariant of our `Aliases` class only belonging to us by
+// itself ourselves otherwise.
+private final function bool AssertAliasesClassIsOwnedByMe()
+{
+ if (aliasesClass == none) return true;
+ if (aliasesClass.default.sourceClass == class) return true;
+ _.logger.Failure("`AliasSource`-`Aliases` class pair is incorrectly"
+ @ "setup for source `" $ string(class) $ "`. Omitting it.");
+ Destroy();
+ 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()
+{
+ local int i;
+ local int entriesAmount;
+ local array objectNames;
+ entriesAmount = record.length;
+ if (aliasesClass == none) {
+ return entriesAmount;
+ }
+ objectNames =
+ GetPerObjectNames(configName, string(aliasesClass.name), MaxInt);
+ loadedAliasObjects.length = objectNames.length;
+ for (i = 0; i < objectNames.length; i += 1)
+ {
+ loadedAliasObjects[i] = new(none, objectNames[i]) aliasesClass;
+ entriesAmount += loadedAliasObjects[i].GetAliases().length;
+ }
+ 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)
+{
+ return hash.Contains(alias);
+}
+
+/**
+ * Tries to look up a value, stored for given alias in caller `AliasSource` and
+ * reports error upon failure.
+ *
+ * Also see `Try()` method.
+ *
+ * @param alias Alias, for which method will attempt to look up a value.
+ * Case-insensitive.
+ * @param value If passed `alias` was recorded in caller `AliasSource`,
+ * it's corresponding value will be written in this variable.
+ * Otherwise value is undefined.
+ * @return `true` if lookup was successful (alias present in 'AliasSource`)
+ * and correct value was written into `value`, `false` otherwise.
+ */
+public function bool Resolve(string alias, out string value)
+{
+ return hash.Find(alias, value);
+}
+
+/**
+ * Tries to look up a value, stored for given alias in caller `AliasSource` and
+ * silently returns given `alias` value upon failure.
+ *
+ * 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.
+ */
+public function string Try(string alias)
+{
+ local string result;
+ if (hash.Find(alias, result)) {
+ return result;
+ }
+ return alias;
+}
+
+/**
+ * Adds another alias to the caller `AliasSource`.
+ * If alias with the same name as `aliasToAdd` already exists, -
+ * method overwrites it.
+ *
+ * Can fail iff `aliasToAdd` is an invalid alias.
+ *
+ * When adding alias to an object (`saveInObject == true`) alias `aliasToAdd`
+ * will be altered by changing any ':' inside it into a '.'.
+ * This is a necessary measure to allow storing class names in
+ * config files via per-object-config.
+ *
+ * NOTE: This call will cause update of an ini-file. That update can be
+ * slightly delayed, so do not make assumptions about it's immediacy.
+ *
+ * NOTE #2: Removing alias would require this method to go through the
+ * whole `AliasSource` to remove possible duplicates.
+ * This means that unless you can guarantee that there is no duplicates, -
+ * performing a lot of alias additions during run-time can be costly.
+ *
+ * @param aliasToAdd Alias that you want to add to caller source.
+ * Alias names are case-insensitive.
+ * @param aliasValue Intended value of this alias.
+ * @param saveInObject Setting this to `true` will make `AliasSource` save
+ * given alias in per-object-config storage, while keeping it at default
+ * `false` will just add alias to the `record=` storage.
+ * If caller `AliasSource` does not support per-object-config storage, -
+ * this flag will be ignores.
+ * @return `true` if alias was added and `false` otherwise (alias was invalid).
+ */
+public final function bool AddAlias(
+ string aliasToAdd,
+ string aliasValue,
+ optional bool saveInObject)
+{
+ local AliasValuePair newPair;
+ if (_.alias.IsAliasValid(aliasToAdd)) {
+ return false;
+ }
+ if (hash.Contains(aliasToAdd)) {
+ 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;
+ record[record.length] = newPair;
+ }
+ hash.Insert(aliasToAdd, aliasValue);
+ AliasService(class'AliasService'.static.Require()).PendingSaveSource(self);
+ return true;
+}
+
+/**
+ * Removes alias (all records with it, in case of duplicates) from
+ * the caller `AliasSource`.
+ *
+ * Cannot fail.
+ *
+ * NOTE: This call will cause update of an ini-file. That update can be
+ * slightly delayed, so do not make assumptions about it's immediacy.
+ *
+ * NOTE #2: removing alias requires this method to go through the
+ * whole `AliasSource` to remove possible duplicates, which can make
+ * performing a lot of alias removal during run-time costly.
+ *
+ * @param aliasToRemove Alias that you want to remove from caller source.
+ */
+public final function RemoveAlias(string aliasToRemove)
+{
+ local int i;
+ local bool removedAliasFromRecord;
+ hash.Remove(aliasToRemove);
+ while (i < record.length)
+ {
+ if (record[i].alias ~= aliasToRemove)
+ {
+ record.Remove(i, 1);
+ removedAliasFromRecord = true;
+ }
+ else {
+ i += 1;
+ }
+ }
+ for (i = 0; i < loadedAliasObjects.length; i += 1) {
+ loadedAliasObjects[i].RemoveAlias(aliasToRemove);
+ }
+ if (removedAliasFromRecord)
+ {
+ AliasService(class'AliasService'.static.Require())
+ .PendingSaveSource(self);
+ }
+}
+
+// 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)
+{
+ _.logger.Warning("Alias source `" $ string(class)
+ $ "` has duplicate record for alias \"" $ alias
+ $ "\". This is likely due to an erroneous config. \"" $ existingValue
+ $ "\" value will be used.");
+}
+
+private final function LogInvalidAliasWarning(string invalidAlias)
+{
+ _.logger.Warning("Alias source `" $ string(class)
+ $ "` contains invalid alias name \"" $ invalidAlias
+ $ "\". 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)
+{
+ local int i;
+ 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) {
+ return loadedAliasObjects[i];
+ }
+ }
+ newAliasesObject = new(none, value) aliasesClass;
+ loadedAliasObjects[loadedAliasObjects.length] = newAliasesObject;
+ return newAliasesObject;
+}
+
+defaultproperties
+{
+ // Source main parameters
+ configName = "AcediaAliases"
+ aliasesClass = class'Aliases'
+ // HashTable twice the size of data entries should do it
+ HASH_TABLE_SCALE = 2.0
+}
\ No newline at end of file
diff --git a/sources/Core/Aliases/Aliases.uc b/sources/Core/Aliases/Aliases.uc
index 4e4ecab..8b30683 100644
--- a/sources/Core/Aliases/Aliases.uc
+++ b/sources/Core/Aliases/Aliases.uc
@@ -1,20 +1,14 @@
/**
- * Aliases allow users to define human-readable and easier to use
- * "synonyms" to some symbol sequences (mainly names of UnrealScript classes).
- * Due to how aliases are stored, there is a limitation on original
- * values to which aliases refer: it must be a valid object name to store via
- * `perObjectConfig`. For example it cannot contain `]` or a dot `.`
- * (use `:` as a delimiter for class names: `KFMod:M14EBRBattleRifle`).
- * Aliases can be grouped into categories: "weapons", "test", "maps", etc.
- * Aliases can be configured in `AcediaAliases` in form:
- * ________________________________________________________________________
- * | [/ Aliases]
- * | Alias=""
- * | Alias=""
- * | ...
- * |_______________________________________________________________________
- * where , , , ... can be replaced with
- * desired values.
+ * This is a simple helper object for `AliasSource` that can store
+ * an array of aliases in config files in a per-object-config manner.
+ * One `Aliases` object can store several aliases for a single value.
+ * It is recommended that you do not try to access these objects directly.
+ * Class name `Aliases` is chosen to make configuration files
+ * more readable.
+ * It's only interesting function is storing '.'s as ':' in it's config,
+ * 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
*------------------------------------------------------------------------------
* This file is part of Acedia.
@@ -36,105 +30,113 @@ class Aliases extends AcediaObject
perObjectConfig
config(AcediaAliases);
-/**
- * All data is stored in config as a bunch of named `Aliases` objects
- * (via `perObjectConfig`). Name of each object records both aliases group and
- * value (see class description for details).
- * Aliases themselves are recorded into the `alias` array.
- */
-
-// Stores name of the configuration file.
-var private const string configName;
-// Both value
-// Symbol (or symbol sequence) that separates value from the group in
-// `[/ Aliases]`.
-var private const string delimiter;
+// 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;
-// Set once to prevent more than one object loading.
-var private bool initialized;
+// Aliases, recorded by this `Aliases` object that all mean the same value,
+// defined by this object's name `string(self.name)`.
+var protected config array alias;
-// All aliases objects, specified by the configuration file.
-var private array availableRecords;
+// Since '.'s in values are converted into ':' for storage purposes,
+// we need methods to convert between "storage" and "actual" value version.
+// `ToStorageVersion()` and `ToActualVersion()` do that.
+private final function string ToStorageVersion(string actualValue)
+{
+ return Repl(actualValue, ".", ":");
+}
-// Data loaded from the configuration file into the `Aliases` object.
-// Value to which all aliases refer to.
-var private string originalValue;
-// Group to which this object's aliases belong to.
-var private string groupName;
-// Recorded aliases ("synonyms") for the `originalValue`.
-var public config array alias;
+private final function string ToActualVersion(string storageValue)
+{
+ return Repl(storageValue, ":", ".");
+}
-// Initializes data that we can not directly read from the configuration file.
-private final function Initialize()
+/**
+ * Returns value that caller's `Aliases` object's aliases point to.
+ *
+ * @return Value, stored by this object.
+ */
+public final function string GetValue()
{
- if (initialized) return;
+ return ToActualVersion(string(self.name));
+}
- availableRecords.length = 0;
- ParseObjectName(string(self.name));
- initialized = true;
+/**
+ * Returns array of aliases that caller `Aliases` tells us point to it's value.
+ *
+ * @return Array of all aliases, stored by caller `Aliases` object.
+ */
+public final function array GetAliases()
+{
+ return alias;
}
-private final function ParseObjectName(string configName)
+/**
+ * [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`,
+ * but will prevent adding duplicate records inside it's own storage.
+ *
+ * @param aliasToAdd Alias to add to caller `Aliases` object.
+ */
+public final function AddAlias(string aliasToAdd)
{
- local int i;
- local array splitName;
- Split(configName, "/", splitName);
- groupName = splitName[0];
- originalValue = "";
- for (i = 1; i < splitName.length; i += 1)
- {
- originalValue $= splitName[i];
+ local int i;
+ for (i = 0; i < alias.length; i += 1) {
+ if (alias[i] ~= aliasToAdd) return;
}
+ alias[alias.length] = ToStorageVersion(aliasToAdd);
+ AliasService(class'AliasService'.static.Require())
+ .PendingSaveObject(self);
}
-// This function loads all the defined aliases from the config file.
-// Need to only be called once, further calls do nothing.
-public static final function LoadAliases()
+/**
+ * [For inner use by `AliasSource`] Removes alias from this object.
+ *
+ * Does not update relevant `AliasHash`.
+ *
+ * Will prevent adding duplicate records inside it's own storage.
+ *
+ * @param aliasToRemove Alias to remove from caller `Aliases` object.
+ */
+public final function RemoveAlias(string aliasToRemove)
{
- local int i;
- local array recordNames;
- if (default.initialized) return;
- recordNames =
- GetPerObjectNames(default.configName, string(class'Aliases'.name));
- for (i = 0; i < recordNames.length; i += 1)
+ local int i;
+ local bool removedAlias;
+ while (i < alias.length)
{
- default.availableRecords[i] = new(none, recordNames[i]) class'Aliases';
- if (default.availableRecords[i] != none)
+ if (alias[i] ~= aliasToRemove)
{
- default.availableRecords[i].Initialize();
+ alias.Remove(i, 1);
+ removedAlias = true;
+ }
+ else {
+ i += 1;
}
}
- default.initialized = true;
+ if (removedAlias)
+ {
+ AliasService(class'AliasService'.static.Require())
+ .PendingSaveObject(self);
+ }
}
-// Tries to find original value for a given alias in a given group.
-public static final function bool ResolveAlias
-(
- string group,
- string alias,
- out string result
-)
+/**
+ * If this object still has any alias records, - forces a rewrite of it's data
+ * into the config file, otherwise - removes it's record entirely.
+ */
+public final function SaveOrClear()
{
- local int i, j;
- if (!default.initialized) return false;
- for (i = 0; i < default.availableRecords.length; i += 1)
- {
- if (!(default.availableRecords[i].groupName ~= group)) continue;
- for (j = 0; j < default.availableRecords[i].alias.length; j += 1)
- {
- if (default.availableRecords[i].alias[j] ~= alias)
- {
- result = default.availableRecords[i].originalValue;
- return true;
- }
- }
+ if (alias.length <= 0) {
+ ClearConfig();
+ }
+ else {
+ SaveConfig();
}
- return false;
}
defaultproperties
{
- initialized = false
- configName = "AcediaAliases"
- delimiter = "/"
+ sourceClass = class'AliasSource'
}
\ No newline at end of file
diff --git a/sources/Core/Aliases/AliasesAPI.uc b/sources/Core/Aliases/AliasesAPI.uc
index 7ebab8c..d231640 100644
--- a/sources/Core/Aliases/AliasesAPI.uc
+++ b/sources/Core/Aliases/AliasesAPI.uc
@@ -19,21 +19,211 @@
*/
class AliasesAPI extends Singleton;
-// Resolves original value for given alias and it's group.
-// Returns `false` if there no such alias and `true` if there is.
-public function bool Resolve(string group, string alias, out string result)
+/**
+ * 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);
+}
+
+/**
+ * Provides an easier access to the instance of the `AliasSource` of
+ * the given class.
+ *
+ * Can fail if `customSourceClass` is incorrectly defined.
+ *
+ * @param customSourceClass Class of the source we want.
+ * @return Instance of the requested `AliasSource`,
+ * `none` if `customSourceClass` is incorrectly defined.
+ */
+public final function AliasSource GetCustomSource(
+ class customSourceClass)
+{
+ return AliasSource(customSourceClass.static.GetInstance(true));
+}
+
+/**
+ * Returns `AliasSource` that is designated in configuration files as
+ * a source for weapon aliases.
+ *
+ * NOTE: while by default weapon aliases source will contain only weapon
+ * aliases, you should not assume that. Acedia allows admins to store all
+ * the aliases in the same config.
+ *
+ * @return Reference to the `AliasSource` that contains weapon aliases.
+ * Can return `none` if no source for weapons was configured or
+ * the configured source is incorrectly defined.
+ */
+public final function AliasSource GetWeaponSource()
+{
+ local AliasSource weaponSource;
+ local class sourceClass;
+ sourceClass = class'AliasService'.default.weaponAliasesSource;
+ if (sourceClass == none) {
+ _.logger.Failure("No weapon aliases source configured for Acedia's"
+ @ "alias API. Error is most likely cause by erroneous config.");
+ return none;
+ }
+ weaponSource = AliasSource(sourceClass.static.GetInstance(true));
+ if (weaponSource == none) {
+ _.logger.Failure("`AliasSource` class `" $ string(sourceClass) $ "` is"
+ @ "configured to store weapon aliases, but it seems to be invalid."
+ @ "This is a bug and not configuration file problem, but issue"
+ @ "might be avoided by using a different `AliasSource`.");
+ return none;
+ }
+ return weaponSource;
+}
+
+/**
+ * Returns `AliasSource` that is designated in configuration files as
+ * a source for color aliases.
+ *
+ * NOTE: while by default color aliases source will contain only color aliases,
+ * you should not assume that. Acedia allows admins to store all the aliases
+ * in the same config.
+ *
+ * @return Reference to the `AliasSource` that contains color aliases.
+ * Can return `none` if no source for colors was configured or
+ * the configured source is incorrectly defined.
+ */
+public final function AliasSource GetColorSource()
+{
+ local AliasSource colorSource;
+ local class sourceClass;
+ sourceClass = class'AliasService'.default.colorAliasesSource;
+ if (sourceClass == none) {
+ _.logger.Failure("No color aliases source configured for Acedia's"
+ @ "alias API. Error is most likely cause by erroneous config.");
+ return none;
+ }
+ colorSource = AliasSource(sourceClass.static.GetInstance(true));
+ if (colorSource == none) {
+ _.logger.Failure("`AliasSource` class `" $ string(sourceClass) $ "` is"
+ @ "configured to store color aliases, but it seems to be invalid."
+ @ "This is a bug and not configuration file problem, but issue"
+ @ "might be avoided by using a different `AliasSource`.");
+ return none;
+ }
+ return colorSource;
+}
+
+/**
+ * Tries to look up a value, stored for given alias in an `AliasSource`
+ * configured to store weapon aliases. Reports error 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
+ * (due to either faulty configuration or incorrect definition).
+ * 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.
+ */
+public final function bool ResolveWeapon(string alias, out string result)
{
- return class'Aliases'.static.ResolveAlias(group, alias, result);
+ local AliasSource source;
+ source = GetWeaponSource();
+ if (source != none) {
+ return source.Resolve(alias, result);
+ }
+ return false;
}
-// Tries to resolve given alias.
-// If fails - returns passed `alias` value back.
-public function string Try(string group, string alias)
+/**
+ * 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`
+ * configured to store color aliases. Reports error on 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 `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.
+ */
+public function string TryColor(string alias)
{
- local string result;
- if (class'Aliases'.static.ResolveAlias(group, alias, result))
- {
- return result;
+ local AliasSource source;
+ source = GetColorSource();
+ if (source != none) {
+ return source.Try(alias);
}
return alias;
}
diff --git a/sources/Core/Aliases/BuiltInSources/ColorAliasSource.uc b/sources/Core/Aliases/BuiltInSources/ColorAliasSource.uc
new file mode 100644
index 0000000..5cd75cf
--- /dev/null
+++ b/sources/Core/Aliases/BuiltInSources/ColorAliasSource.uc
@@ -0,0 +1,27 @@
+/**
+ * Source intended for color aliases.
+ * 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 ColorAliasSource extends AliasSource
+ config(AcediaAliases_Colors);
+
+defaultproperties
+{
+ configName = "AcediaAliases_Colors"
+ aliasesClass = class'ColorAliases'
+}
\ No newline at end of file
diff --git a/sources/Core/Aliases/BuiltInSources/ColorAliases.uc b/sources/Core/Aliases/BuiltInSources/ColorAliases.uc
new file mode 100644
index 0000000..d0998b6
--- /dev/null
+++ b/sources/Core/Aliases/BuiltInSources/ColorAliases.uc
@@ -0,0 +1,27 @@
+/**
+ * Per-object-configuration intended for color aliases.
+ * 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 ColorAliases extends Aliases
+ perObjectConfig
+ config(AcediaAliases_Colors);
+
+defaultproperties
+{
+ sourceClass = class'ColorAliasSource'
+}
\ No newline at end of file
diff --git a/sources/Core/Aliases/BuiltInSources/WeaponAliasSource.uc b/sources/Core/Aliases/BuiltInSources/WeaponAliasSource.uc
new file mode 100644
index 0000000..0cf1bc4
--- /dev/null
+++ b/sources/Core/Aliases/BuiltInSources/WeaponAliasSource.uc
@@ -0,0 +1,27 @@
+/**
+ * Source intended for weapon aliases.
+ * 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 WeaponAliasSource extends AliasSource
+ config(AcediaAliases_Weapons);
+
+defaultproperties
+{
+ configName = "AcediaAliases_Weapons"
+ aliasesClass = class'WeaponAliases'
+}
\ No newline at end of file
diff --git a/sources/Core/Aliases/BuiltInSources/WeaponAliases.uc b/sources/Core/Aliases/BuiltInSources/WeaponAliases.uc
new file mode 100644
index 0000000..82acd45
--- /dev/null
+++ b/sources/Core/Aliases/BuiltInSources/WeaponAliases.uc
@@ -0,0 +1,27 @@
+/**
+ * Per-object-configuration intended for weapon aliases.
+ * 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 WeaponAliases extends Aliases
+ perObjectConfig
+ config(AcediaAliases_Weapons);
+
+defaultproperties
+{
+ sourceClass = class'WeaponAliasSource'
+}
\ No newline at end of file