/** * 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 }