From 27c88b8707aef783f75c4781b4f12e3ae01ea39c Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 18 Jul 2020 02:17:22 +0700 Subject: [PATCH] Refactor Aliases subsystem --- sources/Core/Aliases/AliasHash.uc | 218 ++++++++++ sources/Core/Aliases/AliasService.uc | 135 +++++++ sources/Core/Aliases/AliasSource.uc | 379 ++++++++++++++++++ sources/Core/Aliases/Aliases.uc | 186 ++++----- sources/Core/Aliases/AliasesAPI.uc | 212 +++++++++- .../BuiltInSources/ColorAliasSource.uc | 27 ++ .../Aliases/BuiltInSources/ColorAliases.uc | 27 ++ .../BuiltInSources/WeaponAliasSource.uc | 27 ++ .../Aliases/BuiltInSources/WeaponAliases.uc | 27 ++ 9 files changed, 1135 insertions(+), 103 deletions(-) create mode 100644 sources/Core/Aliases/AliasHash.uc create mode 100644 sources/Core/Aliases/AliasService.uc create mode 100644 sources/Core/Aliases/AliasSource.uc create mode 100644 sources/Core/Aliases/BuiltInSources/ColorAliasSource.uc create mode 100644 sources/Core/Aliases/BuiltInSources/ColorAliases.uc create mode 100644 sources/Core/Aliases/BuiltInSources/WeaponAliasSource.uc create mode 100644 sources/Core/Aliases/BuiltInSources/WeaponAliases.uc 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