/**
* This class implements an alias database that stores aliases inside
* standard config ini-files.
* Copyright 2020-2022 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 BaseAliasSource
dependson(HashTable)
abstract;
// 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;
// (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;
// 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 HashTable aliasHash;
// Faster access to all aliases, corresponding to a certain value.
// This `HashTable` stores same data as `aliasHash`, but "in reverse":
// for each value as a "key" it stored `ArrayList` of corresponding aliases.
var private HashTable valueHash;
// `true` means that this `AliasSource` is awaiting saving into its config
var private bool pendingSaveToConfig;
var private LoggerAPI.Definition errIncorrectAliasPair, warnDuplicateAlias;
var private LoggerAPI.Definition warnInvalidAlias;
// Load and hash all the data `AliasSource` creation.
protected function Constructor()
{
// If this check fails - caller alias source is fundamentally broken
// and requires mod to be fixed
if (!ASSERT_AliasesClassIsOwnedByThisSource()) {
return;
}
// Load and hash
loadedAliasObjects = aliasesClass.static.LoadAllObjects();
aliasHash = _.collections.EmptyHashTable();
valueHash = _.collections.EmptyHashTable();
HashValidAliasesFromRecord();
HashValidAliasesFromPerObjectConfig();
}
protected function Finalizer()
{
loadedAliasObjects.length = 0;
_.memory.Free(aliasHash);
aliasHash = none;
if (pendingSaveToConfig) {
SaveConfig();
}
}
// Ensures that our `Aliases` class is properly linked with this
// source's class. Logs failure otherwise.
private function bool ASSERT_AliasesClassIsOwnedByThisSource()
{
if (aliasesClass == none) return true;
if (aliasesClass.default.sourceClass == class) return true;
_.logger.Auto(errIncorrectAliasPair).ArgClass(class);
return false;
}
// Load hashes from `AliasSource`'s config (`record` array)
private function HashValidAliasesFromRecord()
{
local int i;
local Text aliasAsText, valueAsText;
for (i = 0; i < record.length; i += 1)
{
aliasAsText = _.text.FromString(record[i].alias);
valueAsText = _.text.FromString(record[i].value);
InsertAliasIntoHash(aliasAsText, valueAsText);
aliasAsText.FreeSelf();
valueAsText.FreeSelf();
}
}
// Load hashes from `Aliases` objects' config
private function HashValidAliasesFromPerObjectConfig()
{
local int i, j;
local Text nextValue;
local array valueAliases;
for (i = 0; i < loadedAliasObjects.length; i += 1)
{
nextValue = loadedAliasObjects[i].GetValue();
valueAliases = loadedAliasObjects[i].GetAliases();
for (j = 0; j < valueAliases.length; j += 1) {
InsertAliasIntoHash(valueAliases[j], nextValue);
}
nextValue.FreeSelf();
_.memory.FreeMany(valueAliases);
}
}
public static function bool AreValuesCaseSensitive()
{
// Almost all built-in aliases are aliases to class names (or templates)
// and the rest are colors. Both are case-insensitive, so returning `false`
// is a good default implementation. Child classes can just change this
// value, if they need.
return false;
}
public function array GetAllAliases()
{
return aliasHash.GetTextKeys();
}
public function array GetAliases(BaseText value)
{
local int i;
local Text storedValue;
local ArrayList aliasesArray;
local array result;
storedValue = NormalizeValue(value);
aliasesArray = valueHash.GetArrayList(storedValue);
storedValue.FreeSelf();
if (aliasesArray == none) {
return result;
}
for (i = 0; i < aliasesArray.GetLength(); i += 1) {
result[result.length] = aliasesArray.GetText(i);
}
return result;
}
// "Normalizes" value:
// 1. Converts it into lower case if `AreValuesCaseSensitive()` returns
// `true`;
// 2. Converts in into `Text` in case passed value is `MutableText`, so
// that hash table is actually usable.
private function Text NormalizeValue(BaseText value)
{
if (value == none) {
return none;
}
if (AreValuesCaseSensitive()) {
return value.Copy();
}
return value.LowerCopy();
}
// Inserts alias into `aliasHash`, cleaning previous keys/values in case
// they already exist.
// Takes care of lower case conversion to store aliases in `aliasHash`
// in a case-insensitive way. Depending on `AreValuesCaseSensitive()`, can also
// convert values to lower case.
private function InsertAliasIntoHash(BaseText alias, BaseText value)
{
local Text storedAlias;
local Text storedValue;
local Text existingValue;
local ArrayList valueAliases;
if (alias == none) return;
if (value == none) return;
if (!alias.IsValidName())
{
_.logger.Auto(warnInvalidAlias)
.ArgClass(class)
.Arg(alias.Copy());
return;
}
storedAlias = alias.LowerCopy();
existingValue = aliasHash.GetText(storedAlias);
if (aliasHash.HasKey(storedAlias))
{
_.logger.Auto(warnDuplicateAlias)
.ArgClass(class)
.Arg(alias.Copy())
.Arg(existingValue);
}
_.memory.Free(existingValue);
storedValue = NormalizeValue(value);
// Add to `aliasHash`: alias -> value
aliasHash.SetItem(storedAlias, storedValue);
// Add to `valueHash`: value -> alias
valueAliases = valueHash.GetArrayList(storedValue);
if (valueAliases == none) {
valueAliases = _.collections.EmptyArrayList();
}
valueAliases.AddItem(storedAlias);
valueHash.SetItem(storedValue, valueAliases);
// Clean up
storedAlias.FreeSelf();
storedValue.FreeSelf();
}
public function bool HasAlias(BaseText alias)
{
local bool result;
local Text storedAlias;
if (alias == none) {
return false;
}
storedAlias = alias.LowerCopy();
result = aliasHash.HasKey(storedAlias);
storedAlias.FreeSelf();
return result;
}
public function Text Resolve(
BaseText alias,
optional bool copyOnFailure)
{
local Text result;
local Text storedAlias;
if (alias == none) {
return none;
}
storedAlias = alias.LowerCopy();
result = aliasHash.GetText(storedAlias);
storedAlias.FreeSelf();
if (result != none) {
return result;
}
if (copyOnFailure) {
return alias.Copy();
}
return none;
}
public function bool AddAlias(BaseText aliasToAdd, BaseText aliasValue)
{
local Text storedAlias;
if (aliasToAdd == none) return false;
if (aliasValue == none) return false;
if (!aliasToAdd.IsValidName()) return false;
// Check if alias already exists and if yes - remove it
storedAlias = aliasToAdd.LowerCopy();
if (aliasHash.HasKey(storedAlias)) {
RemoveAlias(aliasToAdd);
}
storedAlias.FreeSelf();
// Add alias-value pair
AddToConfigRecords(aliasToAdd.ToString(), aliasValue.ToString());
InsertAliasIntoHash(aliasToAdd, aliasValue);
return true;
}
public function bool RemoveAlias(BaseText aliasToRemove)
{
local Text storedAlias, storedValue;
local ArrayList valueAliases;
if (aliasToRemove == none) return false;
if (!aliasToRemove.IsValidName()) return false;
storedAlias = aliasToRemove.LowerCopy();
storedValue = aliasHash.GetText(storedAlias);
if (storedValue == none)
{
storedAlias.FreeSelf();
return false;
}
aliasHash.RemoveItem(aliasToRemove);
// Since we've found `storedValue`, this couldn't possibly be `none` if
// "same data invariant" is preserved (see their declaration)
valueAliases = valueHash.GetArrayList(storedValue);
if (valueAliases != none) {
valueAliases.RemoveItem(storedAlias, true);
}
if (valueAliases != none && valueAliases.GetLength() <= 0)
{
valueHash.SetItem(storedValue, none);
valueAliases = none;
}
_.memory.Free(valueAliases);
RemoveFromConfigRecords(aliasToRemove.ToString());
return true;
}
// Takes `string`s that represents alias to remove in proper case (lower for
// aliases and for values it depends on the caller source's settings):
// aliases are supposed to be ASCII, so `string` should handle it and its
// comparison just fine
private function AddToConfigRecords(string alias, string value)
{
local AliasValuePair newPair;
newPair.alias = alias;
newPair.value = value;
record[record.length] = newPair;
// Request saving
if (!pendingSaveToConfig)
{
pendingSaveToConfig = true;
_.scheduler.RequestDiskAccess(self).connect = SaveSelf;
}
}
// Takes `string` that represents alias to remove in lower case: aliases are
// supposed to be ASCII, so `string` should handle it and its comparison just
// fine
private function RemoveFromConfigRecords(string aliasToRemove)
{
local int i;
local bool removedAliasFromRecord;
// Aliases are supposed to be ASCII, so `string` should handle it and its
// comparison just fine
while (i < record.length)
{
if (aliasToRemove ~= record[i].alias)
{
record.Remove(i, 1);
removedAliasFromRecord = true;
}
else {
i += 1;
}
}
// Since admins can fuck up and add duplicate aliases, we need to
// thoroughly check every alias object
for (i = 0; i < loadedAliasObjects.length; i += 1) {
loadedAliasObjects[i].RemoveAlias_S(aliasToRemove);
}
// Alias objects can request disk access themselves, so only record if
// needed for the record
if (removedAliasFromRecord && !pendingSaveToConfig)
{
pendingSaveToConfig = true;
_.scheduler.RequestDiskAccess(self).connect = SaveSelf;
}
}
private function SaveSelf()
{
pendingSaveToConfig = false;
SaveConfig();
}
defaultproperties
{
// Source main parameters
aliasesClass = class'Aliases'
errIncorrectAliasPair = (l=LOG_Error,m="`AliasSource`-`Aliases` class pair is incorrectly setup for source `%1`. Omitting it.")
warnDuplicateAlias = (l=LOG_Warning,m="Alias source `%1` has duplicate record for alias \"%2\". This is likely due to an erroneous config. \"%3\" value will be used.")
warnInvalidAlias = (l=LOG_Warning,m="Alias source `%1` has record with invalid alias \"%2\". This is likely due to an erroneous config. This alias will be discarded.")
}