Browse Source

Refactor Aliases subsystem

new
Anton Tarasenko 4 years ago
parent
commit
27c88b8707
  1. 218
      sources/Core/Aliases/AliasHash.uc
  2. 135
      sources/Core/Aliases/AliasService.uc
  3. 379
      sources/Core/Aliases/AliasSource.uc
  4. 180
      sources/Core/Aliases/Aliases.uc
  5. 210
      sources/Core/Aliases/AliasesAPI.uc
  6. 27
      sources/Core/Aliases/BuiltInSources/ColorAliasSource.uc
  7. 27
      sources/Core/Aliases/BuiltInSources/ColorAliases.uc
  8. 27
      sources/Core/Aliases/BuiltInSources/WeaponAliasSource.uc
  9. 27
      sources/Core/Aliases/BuiltInSources/WeaponAliases.uc

218
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 <https://www.gnu.org/licenses/>.
*/
class AliasHash extends AcediaObject
dependson(AliasSource)
config(AcediaSystem);
// Reasonable lower and upper limits on hash table capacity,
// that will be enforced if user requires something outside those bounds
var private config const int MINIMUM_CAPACITY;
var private config const int MAXIMUM_CAPACITY;
// Bucket of alias-value pairs, with the same alias hash.
struct PairBucket
{
var array<AliasSource.AliasValuePair> pairs;
};
var private array<PairBucket> hashTable;
/**
* Initializes caller `AliasHash`.
*
* Calling this function again will clear all existing data and will create
* a brand new hash table.
*
* @param desiredCapacity Desired capacity of the underlying hash table.
* Will be clamped between `MINIMUM_CAPACITY` and `MAXIMUM_CAPACITY`.
* Not specifying anything as this parameter creates a hash table of
* size `MINIMUM_CAPACITY`.
* @return A reference to a caller object to allow for function chaining.
*/
public final function AliasHash Initialize(optional int desiredCapacity)
{
desiredCapacity = Clamp(desiredCapacity, MINIMUM_CAPACITY,
MAXIMUM_CAPACITY);
hashTable.length = 0;
hashTable.length = desiredCapacity;
return self;
}
// Helper method that is needed as a replacement for `%`, since it is
// an operation on `float`s in UnrealScript and does not have enough precision
// to work with hashes.
// Assumes positive input.
private function int Remainder(int number, int divisor)
{
local int quotient;
quotient = number / divisor;
return (number - quotient * divisor);
}
// Finds indices for:
// 1. Bucked that contains specified alias (`bucketIndex`);
// 2. Pair for specified alias in the bucket's collection (`pairIndex`).
// `bucketIndex` is always found,
// `pairIndex` is valid iff method returns `true`.
private final function bool FindPairIndices(
string alias,
out int bucketIndex,
out int pairIndex)
{
local int i;
local array<AliasSource.AliasValuePair> bucketPairs;
// `Locs()` is used because aliases are case-insensitive.
bucketIndex = _().text.GetHash(Locs(alias));
if (bucketIndex < 0) {
bucketIndex *= -1;
}
bucketIndex = Remainder(bucketIndex, hashTable.length);
// Check if bucket actually has given alias.
bucketPairs = hashTable[bucketIndex].pairs;
for (i = 0; i < bucketPairs.length; i += 1)
{
if (bucketPairs[i].alias ~= alias)
{
pairIndex = i;
return true;
}
}
return false;
}
/**
* Finds a value for a given alias.
*
* @param alias Alias for which we need to find a value.
* Aliases are case-insensitive.
* @param value If given alias is present in caller `AliasHash`, -
* it's value will be written in this variable.
* Otherwise value is undefined.
* @return `true` if we found value, `false` otherwise.
*/
public final function bool Find(string alias, out string value)
{
local int bucketIndex;
local int pairIndex;
if (FindPairIndices(alias, bucketIndex, pairIndex))
{
value = hashTable[bucketIndex].pairs[pairIndex].value;
return true;
}
return false;
}
/**
* Checks if caller `AliasHash` contains given alias.
*
* @param alias Alias to check for belonging to caller `AliasHash`.
* Aliases are case-insensitive.
* @return `true` if caller `AliasHash` contains the value for a given alias
* and `false` otherwise.
*/
public final function bool Contains(string alias)
{
local int bucketIndex;
local int pairIndex;
return FindPairIndices(alias, bucketIndex, pairIndex);
}
/**
* Inserts new record for alias `alias` for value of `value`.
*
* If there is already a value for a given `alias` - it will be overwritten.
*
* @param alias Alias to insert. Aliases are case-insensitive.
* @param value Value for a given alias to store.
* @return A reference to a caller object to allow for function chaining.
*/
public final function AliasHash Insert(string alias, string value)
{
local int bucketIndex;
local int pairIndex;
local AliasSource.AliasValuePair newRecord;
newRecord.value = value;
newRecord.alias = alias;
if (!FindPairIndices(alias, bucketIndex, pairIndex)) {
pairIndex = hashTable[bucketIndex].pairs.length;
}
hashTable[bucketIndex].pairs[pairIndex] = newRecord;
return self;
}
/**
* Inserts new record for alias `alias` for value of `value`.
*
* If there is already a value for a given `alias`, - new value will be
* discarded and `AliasHash` will not be changed.
*
* @param alias Alias to insert. Aliases are case-insensitive.
* @param value Value for a given alias to store.
* @param existingValue Value that will correspond to a given alias after
* this method's execution. If insertion was successful - given `value`,
* otherwise (if there already was a record for an `alias`)
* it will return value that already existed in caller `AliasHash`.
* @return `true` if given alias-value pair was inserted and `false` otherwise.
*/
public final function bool InsertIfMissing(
string alias,
string value,
out string existingValue)
{
local int bucketIndex;
local int pairIndex;
local AliasSource.AliasValuePair newRecord;
newRecord.value = value;
newRecord.alias = alias;
existingValue = value;
if (FindPairIndices(alias, bucketIndex, pairIndex)) {
existingValue = hashTable[bucketIndex].pairs[pairIndex].value;
return false;
}
pairIndex = hashTable[bucketIndex].pairs.length;
hashTable[bucketIndex].pairs[pairIndex] = newRecord;
return true;
}
/**
* Removes record, corresponding to a given alias `alias`.
*
* @param alias Alias for which all records must be removed.
* @return `true` if record was removed, `false` if id did not
* (can only happen when `AliasHash` did not have any records for `alias`).
*/
public final function bool Remove(string alias)
{
local int bucketIndex;
local int pairIndex;
if (FindPairIndices(alias, bucketIndex, pairIndex)) {
hashTable[bucketIndex].pairs.Remove(pairIndex, 1);
return true;
}
return false;
}
defaultproperties
{
MINIMUM_CAPACITY = 10
MAXIMUM_CAPACITY = 100000
}

135
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 <https://www.gnu.org/licenses/>.
*/
class AliasService extends Service
config(AcediaSystem);
// Objects for which we are yet to write configs
var private array<AliasSource> sourcesPendingToSave;
var private array<Aliases> 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<AliasSource> weaponAliasesSource;
var public config const class<AliasSource> 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'
}

379
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 <https://www.gnu.org/licenses/>.
*/
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<Aliases> 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<Aliases> 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<AliasValuePair> 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<string> objectNames;
entriesAmount = record.length;
if (aliasesClass == none) {
return entriesAmount;
}
objectNames =
GetPerObjectNames(configName, string(aliasesClass.name), MaxInt);
loadedAliasObjects.length = objectNames.length;
for (i = 0; i < objectNames.length; i += 1)
{
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<string> objectAliases;
for (i = 0; i < loadedAliasObjects.length; i += 1)
{
objectValue = loadedAliasObjects[i].GetValue();
objectAliases = loadedAliasObjects[i].GetAliases();
for (j = 0; j < objectAliases.length; j += 1)
{
if (!_.alias.IsAliasValid(objectAliases[j]))
{
LogInvalidAliasWarning(objectAliases[j]);
continue;
}
isDuplicate = !hash.InsertIfMissing(objectAliases[j], objectValue,
existingValue);
if (isDuplicate) {
LogDuplicateAliasWarning(objectAliases[j], existingValue);
}
}
}
}
// Tries to find a loaded `Aliases` config object that stores aliases for
// the given value. If such object does not exists - creates a new one.
private final function Aliases GetAliasesObjectWithValue(string value)
{
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
}

180
sources/Core/Aliases/Aliases.uc

@ -1,20 +1,14 @@
/** /**
* Aliases allow users to define human-readable and easier to use * This is a simple helper object for `AliasSource` that can store
* "synonyms" to some symbol sequences (mainly names of UnrealScript classes). * an array of aliases in config files in a per-object-config manner.
* Due to how aliases are stored, there is a limitation on original * One `Aliases` object can store several aliases for a single value.
* values to which aliases refer: it must be a valid object name to store via * It is recommended that you do not try to access these objects directly.
* `perObjectConfig`. For example it cannot contain `]` or a dot `.` * Class name `Aliases` is chosen to make configuration files
* (use `:` as a delimiter for class names: `KFMod:M14EBRBattleRifle`). * more readable.
* Aliases can be grouped into categories: "weapons", "test", "maps", etc. * It's only interesting function is storing '.'s as ':' in it's config,
* Aliases can be configured in `AcediaAliases` in form: * which is necessary to allow storing aliases for class names via
* ________________________________________________________________________ * these objects (since UnrealScript's cannot handle '.'s in object's names
* | [<groupName>/<aliasesValue> Aliases] * in it's configs).
* | Alias="<alias1>"
* | Alias="<alias2>"
* | ...
* |_______________________________________________________________________
* where <groupName>, <aliasesValue>, <alias1>, ... can be replaced with
* desired values.
* Copyright 2020 Anton Tarasenko * Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
@ -36,105 +30,113 @@ class Aliases extends AcediaObject
perObjectConfig perObjectConfig
config(AcediaAliases); config(AcediaAliases);
/** // Link to the `AliasSource` that uses `Aliases` objects of this class.
* All data is stored in config as a bunch of named `Aliases` objects // To ensure that any `Aliases` sub-class only belongs to one `AliasSource`.
* (via `perObjectConfig`). Name of each object records both aliases group and var public const class<AliasSource> sourceClass;
* 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
// `[<groupName>/<aliasesValue> Aliases]`.
var private const string delimiter;
// Set once to prevent more than one object loading. // Aliases, recorded by this `Aliases` object that all mean the same value,
var private bool initialized; // defined by this object's name `string(self.name)`.
var protected config array<string> alias;
// All aliases objects, specified by the configuration file. // Since '.'s in values are converted into ':' for storage purposes,
var private array<Aliases> availableRecords; // 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. private final function string ToActualVersion(string storageValue)
// Value to which all aliases refer to. {
var private string originalValue; return Repl(storageValue, ":", ".");
// Group to which this object's aliases belong to. }
var private string groupName;
// Recorded aliases ("synonyms") for the `originalValue`.
var public config array<string> alias;
// 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)); * Returns array of aliases that caller `Aliases` tells us point to it's value.
initialized = true; *
* @return Array of all aliases, stored by caller `Aliases` object.
*/
public final function array<string> 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 int i;
local array<string> splitName; for (i = 0; i < alias.length; i += 1) {
Split(configName, "/", splitName); if (alias[i] ~= aliasToAdd) return;
groupName = splitName[0];
originalValue = "";
for (i = 1; i < splitName.length; i += 1)
{
originalValue $= splitName[i];
} }
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. * [For inner use by `AliasSource`] Removes alias from this object.
public static final function LoadAliases() *
* 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 int i;
local array<string> recordNames; local bool removedAlias;
if (default.initialized) return; while (i < alias.length)
recordNames =
GetPerObjectNames(default.configName, string(class'Aliases'.name));
for (i = 0; i < recordNames.length; i += 1)
{ {
default.availableRecords[i] = new(none, recordNames[i]) class'Aliases'; if (alias[i] ~= aliasToRemove)
if (default.availableRecords[i] != none)
{ {
default.availableRecords[i].Initialize(); alias.Remove(i, 1);
removedAlias = true;
} }
else {
i += 1;
} }
default.initialized = true;
} }
if (removedAlias)
// 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
)
{ {
local int i, j; AliasService(class'AliasService'.static.Require())
if (!default.initialized) return false; .PendingSaveObject(self);
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 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()
{
if (alias.length <= 0) {
ClearConfig();
} }
else {
SaveConfig();
} }
return false;
} }
defaultproperties defaultproperties
{ {
initialized = false sourceClass = class'AliasSource'
configName = "AcediaAliases"
delimiter = "/"
} }

210
sources/Core/Aliases/AliasesAPI.uc

@ -19,21 +19,211 @@
*/ */
class AliasesAPI extends Singleton; 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. * Checks that passed value is a valid alias name.
public function bool Resolve(string group, string alias, out string result) *
* 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<AliasSource> customSourceClass)
{ {
return class'Aliases'.static.ResolveAlias(group, alias, result); return AliasSource(customSourceClass.static.GetInstance(true));
} }
// Tries to resolve given alias. /**
// If fails - returns passed `alias` value back. * Returns `AliasSource` that is designated in configuration files as
public function string Try(string group, string alias) * 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 string result; local AliasSource weaponSource;
if (class'Aliases'.static.ResolveAlias(group, alias, result)) local class<AliasSource> 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<AliasSource> 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)
{
local AliasSource source;
source = GetWeaponSource();
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 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)
{ {
return result; local AliasSource source;
source = GetColorSource();
if (source != none) {
return source.Try(alias);
} }
return alias; return alias;
} }

27
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 <https://www.gnu.org/licenses/>.
*/
class ColorAliasSource extends AliasSource
config(AcediaAliases_Colors);
defaultproperties
{
configName = "AcediaAliases_Colors"
aliasesClass = class'ColorAliases'
}

27
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 <https://www.gnu.org/licenses/>.
*/
class ColorAliases extends Aliases
perObjectConfig
config(AcediaAliases_Colors);
defaultproperties
{
sourceClass = class'ColorAliasSource'
}

27
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 <https://www.gnu.org/licenses/>.
*/
class WeaponAliasSource extends AliasSource
config(AcediaAliases_Weapons);
defaultproperties
{
configName = "AcediaAliases_Weapons"
aliasesClass = class'WeaponAliases'
}

27
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 <https://www.gnu.org/licenses/>.
*/
class WeaponAliases extends Aliases
perObjectConfig
config(AcediaAliases_Weapons);
defaultproperties
{
sourceClass = class'WeaponAliasSource'
}