diff --git a/config/AcediaAliases.ini b/config/AcediaAliases.ini index 891308c..8f77a09 100644 --- a/config/AcediaAliases.ini +++ b/config/AcediaAliases.ini @@ -1,117 +1,18 @@ -[AcediaCore.FeatureAliasSource] -[AcediaCore:Commands_Feature FeatureAliases] -Alias="commands" -Alias="command" -Alias="comands" -Alias="comand" -Alias="cmds" -Alias="cmd" - -[AcediaCore:Avarice_Feature FeatureAliases] -Alias="Avarice" - -[AcediaFixes:FixAmmoSelling_Feature FeatureAliases] -Alias="FixAmmoSelling" -Alias="FixSelling" -Alias="FixAmmo" -Alias="AmmoSellingFix" -Alias="SellingFix" -Alias="AmmoFix" -Alias="FixAmmoSell" -Alias="FixSell" -Alias="AmmoSellFix" -Alias="SellFix" -Alias="FixAmmoPrinting" -Alias="FixPrinting" -Alias="AmmoPrintingFix" -Alias="PrintingFix" -Alias="FixAmmoPrint" -Alias="FixPrint" -Alias="AmmoPrintFix" -Alias="PrintFix" - -[AcediaFixes:FixDoshSpam_Feature FeatureAliases] -Alias="DoshSpamFix" -Alias="DoshFix" -Alias="CashSpamFix" -Alias="CashFix" -Alias="FixDoshSpamF" -Alias="FixDosh" -Alias="FixCashSpam" -Alias="FixCash" - -[AcediaFixes:FixDualiesCost_Feature FeatureAliases] -Alias="FixDualiesCost" -Alias="FixDualies" -Alias="DualiesCostFix" -Alias="DualiesFix" - -[AcediaFixes:FixFFHack_Feature FeatureAliases] -Alias="FixFFHack" -Alias="FixFriendlyFireHack" -Alias="FixFriendFireHack" -Alias="FFHackFix" -Alias="FriendlyFireHackFix" -Alias="FriendFireHackFix" - -[AcediaFixes:FixInfiniteNades_Feature FeatureAliases] -Alias="FixInfiniteNades" -Alias="FixInfiniteNade" -Alias="FixInfNades" -Alias="FixInfNade" -Alias="FixNades" -Alias="FixNade" -Alias="InfiniteNadesFix" -Alias="InfiniteNadeFix" -Alias="InfNadesFix" -Alias="InfNadeFix" -Alias="NadesFix" -Alias="NadeFix" - -[AcediaFixes:FixInventoryAbuse_Feature FeatureAliases] -Alias="FixInventoryAbuse" -Alias="FixInventory" -Alias="InventoryAbuseFix" -Alias="InventoryFix" - -[AcediaFixes:FixLogSpam_Feature FeatureAliases] -Alias="FixLogSpam" -Alias="FixLog" -Alias="LogSpamFix" -Alias="LogFix" - -[AcediaFixes:FixPipes_Feature FeatureAliases] -Alias="FixPipes" -Alias="FixPipe" -Alias="PipesFix" -Alias="PipeFix" - -[AcediaFixes:FixProjectileFF_Feature FeatureAliases] -Alias="FixProjectileFriendlyFire" -Alias="FixProjectileFF" -Alias="FixProjFriendlyFire" -Alias="FixProjFF" -Alias="FixFriendlyFire" -Alias="FixFF" -Alias="ProjectileFriendlyFireFix" -Alias="ProjectileFFFix" -Alias="ProjFriendlyFireFix" -Alias="ProjFFFix" -Alias="FriendlyFireFix" -Alias="FFFix" - -[AcediaFixes:FixSpectatorCrash_Feature FeatureAliases] -Alias="FixSpectatorCrash" -Alias="FixSpecCrash" -Alias="SpectatorCrashFix" -Alias="SpecCrashFix" - -[AcediaFixes:FixZedTimeLags_Feature FeatureAliases] -Alias="FixZedTimeLags" -Alias="FixZedTime" -Alias="FixZTLags" -Alias="FixZT" -Alias="ZedTimeLagsFix" -Alias="ZedTimeFix" -Alias="ZTLagsFix" -Alias="ZTFix" \ No newline at end of file +; NOTE: You most likely do not want to modify this file, unless you are really +; sure what you're doing or was told exactly what to do by someone. +[default Aliases] +; This feature allows you to configure what alias sources are used for resolving +; what alias types. Like all Acedia's features, it supports multiple named +; configs, allowing you to, say, replacve `weaponAliasSoruce` with a different +; one when running a mod that replaces stock weapons. +autoEnable=true +weaponAliasSource=Class'AcediaCore.WeaponAliasSource' +colorAliasSource=Class'AcediaCore.ColorAliasSource' +featureAliasSource=Class'AcediaCore.FeatureAliasSource' +entityAliasSource=Class'AcediaCore.EntityAliasSource' +; `customSource` records allow you to add custom alias sources into Acedia, +; that is sources that do not fit either of the standard groups. These sources +; can then be used by mods to resolve custom aliases using specified `name`. +; Their names, unlike aliases, are case-sensitive. +;customSource=(name="custom",source=Class'BlaBla.CustomSource') +customSource=(name="mock",source=Class'AcediaCore.MockAliasSource') \ No newline at end of file diff --git a/config/AcediaAliases_Colors.ini b/config/AcediaAliases_Colors.ini index 7bb5141..913130d 100644 --- a/config/AcediaAliases_Colors.ini +++ b/config/AcediaAliases_Colors.ini @@ -1,3 +1,5 @@ +; This config file allows you to configure color aliases. +; Remember that aliases are case-insensitive. [AcediaCore.ColorAliasSource] ; System colors record=(alias="TextDefault",value="rgb(255,255,255)") diff --git a/config/AcediaAliases_Entities.ini b/config/AcediaAliases_Entities.ini index 6a048b3..43f6a98 100644 --- a/config/AcediaAliases_Entities.ini +++ b/config/AcediaAliases_Entities.ini @@ -1,3 +1,5 @@ +; This config file allows you to configure entity aliases. +; Remember that aliases are case-insensitive. [AcediaCore.EntityAliasSource] ; Standard zeds [KFChar:ZombieClot_STANDARD EntityAliases] diff --git a/config/AcediaAliases_Features.ini b/config/AcediaAliases_Features.ini new file mode 100644 index 0000000..80711b6 --- /dev/null +++ b/config/AcediaAliases_Features.ini @@ -0,0 +1,119 @@ +; This config file allows you to configure feature aliases. +; Remember that aliases are case-insensitive. +[AcediaCore.FeatureAliasSource] +[AcediaCore:Commands_Feature FeatureAliases] +Alias="commands" +Alias="command" +Alias="comands" +Alias="comand" +Alias="cmds" +Alias="cmd" + +[AcediaCore:Avarice_Feature FeatureAliases] +Alias="Avarice" + +[AcediaFixes:FixAmmoSelling_Feature FeatureAliases] +Alias="FixAmmoSelling" +Alias="FixSelling" +Alias="FixAmmo" +Alias="AmmoSellingFix" +Alias="SellingFix" +Alias="AmmoFix" +Alias="FixAmmoSell" +Alias="FixSell" +Alias="AmmoSellFix" +Alias="SellFix" +Alias="FixAmmoPrinting" +Alias="FixPrinting" +Alias="AmmoPrintingFix" +Alias="PrintingFix" +Alias="FixAmmoPrint" +Alias="FixPrint" +Alias="AmmoPrintFix" +Alias="PrintFix" + +[AcediaFixes:FixDoshSpam_Feature FeatureAliases] +Alias="DoshSpamFix" +Alias="DoshFix" +Alias="CashSpamFix" +Alias="CashFix" +Alias="FixDoshSpamF" +Alias="FixDosh" +Alias="FixCashSpam" +Alias="FixCash" + +[AcediaFixes:FixDualiesCost_Feature FeatureAliases] +Alias="FixDualiesCost" +Alias="FixDualies" +Alias="DualiesCostFix" +Alias="DualiesFix" + +[AcediaFixes:FixFFHack_Feature FeatureAliases] +Alias="FixFFHack" +Alias="FixFriendlyFireHack" +Alias="FixFriendFireHack" +Alias="FFHackFix" +Alias="FriendlyFireHackFix" +Alias="FriendFireHackFix" + +[AcediaFixes:FixInfiniteNades_Feature FeatureAliases] +Alias="FixInfiniteNades" +Alias="FixInfiniteNade" +Alias="FixInfNades" +Alias="FixInfNade" +Alias="FixNades" +Alias="FixNade" +Alias="InfiniteNadesFix" +Alias="InfiniteNadeFix" +Alias="InfNadesFix" +Alias="InfNadeFix" +Alias="NadesFix" +Alias="NadeFix" + +[AcediaFixes:FixInventoryAbuse_Feature FeatureAliases] +Alias="FixInventoryAbuse" +Alias="FixInventory" +Alias="InventoryAbuseFix" +Alias="InventoryFix" + +[AcediaFixes:FixLogSpam_Feature FeatureAliases] +Alias="FixLogSpam" +Alias="FixLog" +Alias="LogSpamFix" +Alias="LogFix" + +[AcediaFixes:FixPipes_Feature FeatureAliases] +Alias="FixPipes" +Alias="FixPipe" +Alias="PipesFix" +Alias="PipeFix" + +[AcediaFixes:FixProjectileFF_Feature FeatureAliases] +Alias="FixProjectileFriendlyFire" +Alias="FixProjectileFF" +Alias="FixProjFriendlyFire" +Alias="FixProjFF" +Alias="FixFriendlyFire" +Alias="FixFF" +Alias="ProjectileFriendlyFireFix" +Alias="ProjectileFFFix" +Alias="ProjFriendlyFireFix" +Alias="ProjFFFix" +Alias="FriendlyFireFix" +Alias="FFFix" + +[AcediaFixes:FixSpectatorCrash_Feature FeatureAliases] +Alias="FixSpectatorCrash" +Alias="FixSpecCrash" +Alias="SpectatorCrashFix" +Alias="SpecCrashFix" + +[AcediaFixes:FixZedTimeLags_Feature FeatureAliases] +Alias="FixZedTimeLags" +Alias="FixZedTime" +Alias="FixZTLags" +Alias="FixZT" +Alias="ZedTimeLagsFix" +Alias="ZedTimeFix" +Alias="ZTLagsFix" +Alias="ZTFix" \ No newline at end of file diff --git a/config/AcediaAliases_Tests.ini b/config/AcediaAliases_Tests.ini index a82a0cd..8c687e2 100644 --- a/config/AcediaAliases_Tests.ini +++ b/config/AcediaAliases_Tests.ini @@ -6,7 +6,6 @@ [AcediaCore.MockAliasSource] record=(alias="global",value="value") record=(alias="question",value="response") -record=(alias="",value="empty") record=(alias="also",value="") [car MockAliases] Alias="Ford" diff --git a/config/AcediaAliases_Weapons.ini b/config/AcediaAliases_Weapons.ini index a0f4855..ea29b70 100644 --- a/config/AcediaAliases_Weapons.ini +++ b/config/AcediaAliases_Weapons.ini @@ -1,3 +1,5 @@ +; This config file allows you to configure weapon aliases. +; Remember that aliases are case-insensitive. [AcediaCore.WeaponAliasSource] ; Field Medic weapons [KFMod:MP7MMedicGun WeaponAliases] @@ -133,44 +135,34 @@ Alias="HuntingShotgun" Alias="BoomStick" Alias="Hunting" [KFMod:KSGShotgun WeaponAliases] -Alias="HSG-1Shotgun" Alias="HSG1Shotgun" Alias="HSGShotgun" Alias="HSG" -Alias="KSG-1Shotgun" Alias="KSG1Shotgun" Alias="KSGShotgun" Alias="KSG" -Alias="HSG-1Shotgun0" Alias="HSG1Shotgun0" Alias="HSGShotgun0" Alias="HSG0" -Alias="KSG-1Shotgun0" Alias="KSG1Shotgun0" Alias="KSGShotgun0" Alias="KSG0" [KFMod:NeonKSGShotgun WeaponAliases] -Alias="HSG-1Shotgun1" Alias="HSG1Shotgun1" Alias="HSGShotgun1" Alias="HSG1" -Alias="KSG-1Shotgun1" Alias="KSG1Shotgun1" Alias="KSGShotgun1" Alias="KSG1" -Alias="NeonHSG-1Shotgun" Alias="NeonHSG1Shotgun" Alias="NeonHSGShotgun" Alias="NeonHSG" -Alias="NeonKSG-1Shotgun" Alias="NeonKSG1Shotgun" Alias="NeonKSGShotgun" Alias="NeonKSG" -Alias="HSG-1ShotgunNeon" Alias="HSG1ShotgunNeon" Alias="HSGShotgunNeon" Alias="HSGNeon" -Alias="KSG-1ShotgunNeon" Alias="KSG1ShotgunNeon" Alias="KSGShotgunNeon" Alias="KSGNeon" @@ -593,13 +585,9 @@ Alias="SteamTomySMG" Alias="SteamTomy" Alias="TomySMGSteam" Alias="TomySteam" -Alias="Dr.T'sLeadDeliverySystem" Alias="Dr.TsLeadDeliverySystem" -Alias="DrT'sLeadDeliverySystem" Alias="DrTsLeadDeliverySystem" -Alias="Dr.T'LeadDeliverySystem" Alias="Dr.TLeadDeliverySystem" -Alias="DrT'LeadDeliverySystem" Alias="DrTLeadDeliverySystem" Alias="DrTDeliverySystem" Alias="DrTLeadSystem" diff --git a/config/AcediaSystem.ini b/config/AcediaSystem.ini index 17d627b..ac5dbc6 100644 --- a/config/AcediaSystem.ini +++ b/config/AcediaSystem.ini @@ -119,17 +119,6 @@ levelStamp=true ; centralized manner. autoEnable=true -[AcediaCore.AliasService] -; Changing these allows you to change in what sources `AliasesAPI` -; looks for weapon and color aliases. -weaponAliasesSource=Class'WeaponAliasSource' -colorAliasesSource=Class'ColorAliasSource' -featureAliasesSource=Class'FeatureAliasSource' -; How often are different alias-storing objects are allowed to record -; their updated data into a config. -; Negative or zero values would be reset to `0.05`. -saveInterval=0.05 - [AcediaCore.TestingService] ; Allows you to run tests on server's start up. This option is to help run ; tests quicker during development and should not be used for servers that are diff --git a/sources/Aliases/AliasService.uc b/sources/Aliases/AliasService.uc deleted file mode 100644 index 980a2f2..0000000 --- a/sources/Aliases/AliasService.uc +++ /dev/null @@ -1,139 +0,0 @@ -/** - * 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; -var public config const class featureAliasesSource; -var public config const class entityAliasesSource; - -protected simulated function OnLaunch() -{ - local float actualInterval; - actualInterval = saveInterval; - if (actualInterval <= 0) - { - actualInterval = 0.05; - } - SetTimer(actualInterval, true); -} - -protected simulated function OnShutdown() -{ - SaveAllPendingObjects(); -} - -public simulated 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 simulated 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 simulated 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 simulated 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; -} - -public simulated function Timer() -{ - DoSaveNextPendingObject(); -} - -defaultproperties -{ - saveInterval = 0.05 - weaponAliasesSource = class'WeaponAliasSource' - colorAliasesSource = class'ColorAliasSource' - featureAliasesSource = class'FeatureAliasSource' - entityAliasesSource = class'EntityAliasSource' -} \ No newline at end of file diff --git a/sources/Aliases/AliasSource.uc b/sources/Aliases/AliasSource.uc index 0baff89..340a923 100644 --- a/sources/Aliases/AliasSource.uc +++ b/sources/Aliases/AliasSource.uc @@ -1,11 +1,7 @@ /** - * 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 - 2021 Anton Tarasenko + * Copyright 2020-2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -22,18 +18,9 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class AliasSource extends Singleton +class AliasSource extends BaseAliasSource dependson(HashTable) - config(AcediaAliases); - -// (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; + abstract; // Links alias to a value. // An array of these structures (without duplicate `alias` records) defines @@ -46,153 +33,222 @@ struct AliasValuePair // 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 simulated function OnCreated() +protected function Constructor() { - if (!AssertAliasesClassIsOwnedByThisSource()) { - Destroy(); + // 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 simulated function OnDestroyed() +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 simulated final function bool AssertAliasesClassIsOwnedByThisSource() +private function bool ASSERT_AliasesClassIsOwnedByThisSource() { if (aliasesClass == none) return true; if (aliasesClass.default.sourceClass == class) return true; + _.logger.Auto(errIncorrectAliasPair).ArgClass(class); - Destroy(); return false; } // Load hashes from `AliasSource`'s config (`record` array) -private simulated final function HashValidAliasesFromRecord() +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); - InsertAlias(aliasAsText, valueAsText); + InsertAliasIntoHash(aliasAsText, valueAsText); aliasAsText.FreeSelf(); valueAsText.FreeSelf(); } } // Load hashes from `Aliases` objects' config -private simulated final function HashValidAliasesFromPerObjectConfig() +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) { - InsertAlias(valueAliases[j], nextValue); + 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 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. -private simulated final function InsertAlias(BaseText alias, BaseText value) +// in a case-insensitive way. Depending on `AreValuesCaseSensitive()`, can also +// convert values to lower case. +private function InsertAliasIntoHash(BaseText alias, BaseText value) { - local Text aliasLowerCaseCopy; - local HashTable.Entry hashEntry; + local Text storedAlias; + local Text storedValue; + local Text existingValue; + local ArrayList valueAliases; + if (alias == none) return; if (value == none) return; - aliasLowerCaseCopy = alias.LowerCopy(); - hashEntry = aliasHash.TakeEntry(aliasLowerCaseCopy); - if (hashEntry.value != none) { - LogDuplicateAliasWarning(alias, Text(hashEntry.value)); + + if (!alias.IsValidName()) + { + _.logger.Auto(warnInvalidAlias) + .ArgClass(class) + .Arg(alias.Copy()); + return; } - _.memory.Free(hashEntry.key); - _.memory.Free(hashEntry.value); - aliasHash.SetItem(aliasLowerCaseCopy, value); - aliasLowerCaseCopy.FreeSelf(); + 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(); } -/** - * Checks if given alias is present in caller `AliasSource`. - * - * @param alias Alias to check, case-insensitive. - * @return `true` if present, `false` otherwise. - */ -public simulated function bool HasAlias(BaseText alias) +public function bool HasAlias(BaseText alias) { local bool result; - local Text lowerCaseAlias; + local Text storedAlias; + if (alias == none) { return false; } - lowerCaseAlias = alias.LowerCopy(); - result = aliasHash.HasKey(lowerCaseAlias); - lowerCaseAlias.FreeSelf(); + storedAlias = alias.LowerCopy(); + result = aliasHash.HasKey(storedAlias); + storedAlias.FreeSelf(); return result; } -/** - * Return value stored for the given alias in caller `AliasSource` - * (as well as it's `Aliases` objects). - * - * @param alias Alias, for which method will attempt to return - * a value. Case-insensitive. If given `alias` starts with "$" character - - * that character will be removed before resolving that alias. - * @param copyOnFailure Whether method should return copy of original - * `alias` value in case caller source did not have any records - * corresponding to `alias`. - * @return If look up was successful - value, associated with the given - * alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure` - * flag: `copyOnFailure == false` means method will return `none` - * and `copyOnFailure == true` means method will return `alias.Copy()`. - * If `alias == none` method always returns `none`. - */ -public simulated function Text Resolve( +public function Text Resolve( BaseText alias, optional bool copyOnFailure) { local Text result; - local Text lowerCaseAlias; + local Text storedAlias; + if (alias == none) { return none; } - // Automatically get rid of "$" prefix, if present - if (alias.StartsWith(P("$"))) { - lowerCaseAlias = alias.LowerCopy(1); - } - else { - lowerCaseAlias = alias.LowerCopy(); - } - result = Text(aliasHash.GetItem(lowerCaseAlias)); - lowerCaseAlias.FreeSelf(); + storedAlias = alias.LowerCopy(); + result = aliasHash.GetText(storedAlias); + storedAlias.FreeSelf(); if (result != none) { return result; } @@ -202,99 +258,90 @@ public simulated function Text Resolve( return none; } -/** - * 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 or `aliasValue == none`. - * - * 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 simulated final function bool AddAlias( - Text aliasToAdd, - Text aliasValue, - optional bool saveInObject) +public function bool AddAlias(BaseText aliasToAdd, BaseText aliasValue) { - local Text lowerCaseAlias; - local AliasValuePair newPair; - if (aliasToAdd == none) return false; - if (aliasValue == none) return false; + local Text storedAlias; - lowerCaseAlias = aliasToAdd.LowerCopy(); - if (aliasHash.HasKey(lowerCaseAlias)) { + 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); } - // Save - if (saveInObject) { - GetAliasesObjectWithValue(aliasValue).AddAlias(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); } - else + if (valueAliases != none && valueAliases.GetLength() <= 0) { - newPair.alias = aliasToAdd.ToString(); - newPair.value = aliasValue.ToString(); - record[record.length] = newPair; + valueHash.SetItem(storedValue, none); + valueAliases = none; } - aliasHash.SetItem(lowerCaseAlias, aliasValue); - _.memory.Free(lowerCaseAlias); - _.memory.Free(aliasValue); - AliasService(class'AliasService'.static.Require()).PendingSaveSource(self); + _.memory.Free(valueAliases); + RemoveFromConfigRecords(aliasToRemove.ToString()); 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 simulated final function RemoveAlias(BaseText aliasToRemove) +// 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 int i; - local bool isMatchingRecord; - local bool removedAliasFromRecord; - local HashTable.Entry hashEntry; - if (aliasToRemove == none) { - return; + local AliasValuePair newPair; + + newPair.alias = alias; + newPair.value = value; + record[record.length] = newPair; + // Request saving + if (!pendingSaveToConfig) + { + pendingSaveToConfig = true; + _.scheduler.RequestDiskAccess(self).connect = SaveSelf; } - hashEntry = aliasHash.TakeEntry(aliasToRemove); - _.memory.Free(hashEntry.key); - _.memory.Free(hashEntry.value); +} + +// 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) { - isMatchingRecord = aliasToRemove - .CompareToString(record[i].alias, SCASE_INSENSITIVE); - if (isMatchingRecord) + if (aliasToRemove ~= record[i].alias) { record.Remove(i, 1); removedAliasFromRecord = true; @@ -303,46 +350,24 @@ public simulated final function RemoveAlias(BaseText aliasToRemove) 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(aliasToRemove); + loadedAliasObjects[i].RemoveAlias_S(aliasToRemove); } - if (removedAliasFromRecord) + // Alias objects can request disk access themselves, so only record if + // needed for the record + if (removedAliasFromRecord && !pendingSaveToConfig) { - AliasService(class'AliasService'.static.Require()) - .PendingSaveSource(self); + pendingSaveToConfig = true; + _.scheduler.RequestDiskAccess(self).connect = SaveSelf; } } -private simulated final function LogDuplicateAliasWarning( - BaseText alias, - BaseText existingValue) +private function SaveSelf() { - _.logger.Auto(warnDuplicateAlias) - .ArgClass(class) - .Arg(alias.Copy()) - .Arg(existingValue.Copy()); -} - -// 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. -// Assumes `value != none`. -private simulated final function Aliases GetAliasesObjectWithValue( - BaseText value) -{ - local int i; - local Text nextValue; - local Aliases newAliasesObject; - for (i = 0; i < loadedAliasObjects.length; i += 1) - { - nextValue = loadedAliasObjects[i].GetValue(); - if (value.Compare(nextValue)) { - return loadedAliasObjects[i]; - } - _.memory.Free(nextValue); - } - newAliasesObject = aliasesClass.static.LoadObject(value); - loadedAliasObjects[loadedAliasObjects.length] = newAliasesObject; - return newAliasesObject; + pendingSaveToConfig = false; + SaveConfig(); } defaultproperties @@ -351,4 +376,5 @@ defaultproperties 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.") } \ No newline at end of file diff --git a/sources/Aliases/Aliases.uc b/sources/Aliases/Aliases.uc index 58014f4..f1bd8f2 100644 --- a/sources/Aliases/Aliases.uc +++ b/sources/Aliases/Aliases.uc @@ -1,15 +1,6 @@ /** - * 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 2019 - 2021 Anton Tarasenko + * Config object for `Aliases_Feature`. + * Copyright 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -26,184 +17,118 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class Aliases extends AcediaObject - perObjectConfig +class Aliases extends FeatureConfig + perobjectconfig config(AcediaAliases); -// Name of the configurational file (without extension) where -// this `AliasSource`'s data will be stored. -var protected const string configName; - -// 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; - -// 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; - -// Since: -// 1. '.'s in values are converted into ':' for storage purposes; -// 2. We have to store values in `string` to make use of config files. -// we need methods to convert between "storage" (`string`) -// and "actual" (`Text`) value version. -// `ToStorageVersion()` and `ToActualVersion()` do that. -private final static function string ToStorageVersion(BaseText actualValue) -{ - return Repl(actualValue.ToString(), ".", ":"); -} - -// See comment to `ToStorageVersion()`. -private final static function Text ToActualVersion(string storageValue) +struct CustomSourceRecord { - return __().text.FromString(Repl(storageValue, ":", ".")); -} + var string name; + var class source; +}; -/** - * Loads all `Aliases` objects from their config file - * (defined in paired `AliasSource` class). - * - * @return Array of all `Aliases` objects, loaded from their config file. - */ -public static final function array LoadAllObjects() -{ - local int i; - local array objectNames; - local array loadedAliasObjects; - objectNames = GetPerObjectNames(default.configName, - string(default.class.name), MaxInt); - for (i = 0; i < objectNames.length; i += 1) { - loadedAliasObjects[i] = LoadObjectByName(objectNames[i]); - } - return loadedAliasObjects; -} +var public config class weaponAliasSource; +var public config class colorAliasSource; +var public config class featureAliasSource; +var public config class entityAliasSource; +var public config array customSource; -// Loads a new `Aliases` object by it's given name (`objectName`). -private static final function Aliases LoadObjectByName(string objectName) +protected function HashTable ToData() { - local Aliases result; - // Since `MemoryAPI` for now does not support specifying names - // to created objects - do some manual dark magic and - // initialize this shit ourselves - result = new(none, objectName) default.class; - result._constructor(); - return result; -} + local int i; + local Text nextKey; + local HashTable data, otherSourcesData; -/** - * Loads a new `Aliases` object based on the value (`aliasesValue`) - * of it's aliases. - * - * @param aliasesValue Value that aliases in this `Aliases` object will - * correspond to. - * @return Instance of `Aliases` object with a given name. - */ -public static final function Aliases LoadObject(BaseText aliasesValue) -{ - if (aliasesValue != none) { - return LoadObjectByName(ToStorageVersion(aliasesValue)); + data = __().collections.EmptyHashTable(); + // Add named aliases + data.SetString(P("weapon"), string(weaponAliasSource)); + data.SetString(P("color"), string(colorAliasSource)); + data.SetString(P("feature"), string(featureAliasSource)); + data.SetString(P("entity"), string(entityAliasSource)); + // Add the rest + otherSourcesData = __().collections.EmptyHashTable(); + for (i = 0; i < customSource.length; i += 1) + { + nextKey = _.text.FromString(customSource[i].name); + otherSourcesData.SetString(nextKey, string(customSource[i].source)); + nextKey.FreeSelf(); } - return none; + data.SetItem(P("other"), otherSourcesData); + otherSourcesData.FreeSelf(); + return data; } -/** - * Returns value that caller's `Aliases` object's aliases point to. - * - * @return Value, stored by this object. - */ -public final function Text GetValue() +protected function FromData(HashTable source) { - return ToActualVersion(string(self.name)); -} + local HashTable otherSourcesData; -/** - * 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() -{ - local int i; - local array textAliases; - for (i = 0; i < alias.length; i += 1) { - textAliases[i] = _.text.FromString(alias[i]); + if (source == none) { + return; } - return textAliases; -} - -/** - * [For inner use by `AliasSource`] Adds new alias to this object. - * - * Does no duplicates checks through for it's `AliasSource` and - * neither does it update relevant `AliasHash`, - * but will prevent adding duplicate records inside it's own storage. - * - * @param aliasToAdd Alias to add to caller `Aliases` object. - * If `none`, method will do nothing. - */ -public final function AddAlias(BaseText aliasToAdd) -{ - local int i; - if (aliasToAdd == none) return; - for (i = 0; i < alias.length; i += 1) + // We cast `class` into `string` + // (e.g. `string(class'AcediaAliases_Weapons')`) + // instead of writing full name of the class so that code is independent + // from this package's name, making it easier to change later + weaponAliasSource = class(_.memory.LoadClass_S( + source.GetString(P("weapon"), string(class'WeaponAliasSource')))); + colorAliasSource = class(_.memory.LoadClass_S( + source.GetString(P("color"), string(class'ColorAliasSource')))); + featureAliasSource = class(_.memory.LoadClass_S( + source.GetString(P("feature"), string(class'FeatureAliasSource')))); + entityAliasSource = class(_.memory.LoadClass_S( + source.GetString(P("entity"), string(class'EntityAliasSource')))); + otherSourcesData = source.GetHashTable(P("other")); + if (otherSourcesData != none) { - if (aliasToAdd.CompareToString(alias[i], SCASE_INSENSITIVE)) { - return; - } + ReadOtherSources(otherSourcesData); + otherSourcesData.FreeSelf(); } - alias[alias.length] = aliasToAdd.ToString(); - AliasService(class'AliasService'.static.Require()) - .PendingSaveObject(self); } -/** - * [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(BaseText aliasToRemove) +// Doesn't check whether `otherSources` is `none` +protected function ReadOtherSources(HashTable otherSourcesData) { - local int i; - local bool removedAlias; - if (aliasToRemove == none) return; - while (i < alias.length) + local CustomSourceRecord newRecord; + local BaseText keyAsText, valueAsText; + local AcediaObject key, value; + local HashTableIterator iter; + + customSource.length = 0; + iter = HashTableIterator(otherSourcesData.Iterate().LeaveOnlyNotNone()); + for (iter = iter; !iter.HasFinished(); iter.Next()) { - if (aliasToRemove.CompareToString(alias[i], SCASE_INSENSITIVE)) + key = iter.GetKey(); + value = iter.GetKey(); + keyAsText = BaseText(key); + valueAsText = BaseText(value); + if (keyAsText != none && valueAsText != none) { - alias.Remove(i, 1); - removedAlias = true; - } - else { - i += 1; + newRecord.name = keyAsText.ToString(); + newRecord.source = class( + _.memory.LoadClass_S(valueAsText.ToString())); + if (newRecord.source != none) { + customSource[customSource.length] = newRecord; + } } - } - if (removedAlias) - { - AliasService(class'AliasService'.static.Require()) - .PendingSaveObject(self); + _.memory.Free(key); + _.memory.Free(value); } } -/** - * 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() +protected function DefaultIt() { - if (alias.length <= 0) { - ClearConfig(); - } - else { - SaveConfig(); - } + customSource.length = 0; + weaponAliasSource = class'WeaponAliasSource'; + colorAliasSource = class'ColorAliasSource'; + featureAliasSource = class'FeatureAliasSource'; + entityAliasSource = class'EntityAliasSource'; } defaultproperties { - sourceClass = class'AliasSource' - configName = "AcediaAliases" + configName = "AcediaAliases" + weaponAliasSource = class'WeaponAliasSource' + colorAliasSource = class'ColorAliasSource' + featureAliasSource = class'FeatureAliasSource' + entityAliasSource = class'EntityAliasSource' } \ No newline at end of file diff --git a/sources/Aliases/AliasesAPI.uc b/sources/Aliases/AliasesAPI.uc index 8b63dd4..e58261d 100644 --- a/sources/Aliases/AliasesAPI.uc +++ b/sources/Aliases/AliasesAPI.uc @@ -1,6 +1,6 @@ /** * Provides convenient access to Aliases-related functions. - * Copyright 2020 - 2021 Anton Tarasenko + * Copyright 2020-2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -20,154 +20,169 @@ class AliasesAPI extends AcediaObject dependson(LoggerAPI); -var private LoggerAPI.Definition noWeaponAliasSource, invalidWeaponAliasSource; -var private LoggerAPI.Definition noColorAliasSource, invalidColorAliasSource; -var private LoggerAPI.Definition noFeatureAliasSource, invalidFeatureAliasSource; -var private LoggerAPI.Definition noEntityAliasSource, invalidEntityAliasSource; +// To avoid bothering with fetching `Aliases_Feature` each time we need to +// access an alias source, we save all the basic ones separately and +// `Aliases_Feature` can simply trigger their updates whenever necessary via +// `AliasesAPI._reloadSources()` function. +var private BaseAliasSource weaponAliasSource; +var private BaseAliasSource colorAliasSource; +var private BaseAliasSource featureAliasSource; +var private BaseAliasSource entityAliasSource; + +public function _reloadSources() +{ + local Aliases_Feature feature; + + _.memory.Free(weaponAliasSource); + _.memory.Free(colorAliasSource); + _.memory.Free(featureAliasSource); + _.memory.Free(entityAliasSource); + weaponAliasSource = none; + colorAliasSource = none; + featureAliasSource = none; + entityAliasSource = none; + feature = Aliases_Feature( + class'Aliases_Feature'.static.GetEnabledInstance()); + if (feature == none) { + return; + } + weaponAliasSource = feature.GetWeaponSource(); + colorAliasSource = feature.GetColorSource(); + featureAliasSource = feature.GetFeatureSource(); + entityAliasSource = feature.GetEntitySource(); + _.memory.Free(feature); +} /** - * Provides an easier access to the instance of the `AliasSource` of - * the given class. + * Provides an easier access to the instance of the custom `BaseAliasSource` + * with a given name `sourceName`. * - * Can fail if `customSourceClass` is incorrectly defined. + * Custom alias sources can be added manually by the admin through the + * `Aliases_Feature` config file.. * - * @param customSourceClass Class of the source we want. - * @return Instance of the requested `AliasSource`, - * `none` if `customSourceClass` is incorrectly defined. + * @param sourceName Name that alias source was added as to + * `Aliases_Feature`. + * @return Instance of the requested `BaseAliasSource`, + * `none` if `sourceName` is `none`, does not refer to any alias source + * or `Aliases_Feature` is disabled. */ -public final function AliasSource GetCustomSource( - class customSourceClass) +public final function BaseAliasSource GetCustomSource(BaseText sourceName) { - return AliasSource(customSourceClass.static.GetInstance(true)); + local Aliases_Feature feature; + + if (sourceName == none) { + return none; + } + feature = + Aliases_Feature(class'Aliases_Feature'.static.GetEnabledInstance()); + if (feature != none) { + return feature.GetCustomSource(sourceName); + } + return none; } /** - * Returns `AliasSource` that is designated in configuration files as + * Returns `BaseAliasSource` 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. + * @return Reference to the `BaseAliasSource` 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() +public final function BaseAliasSource GetWeaponSource() { - local AliasSource weaponSource; - local class sourceClass; - sourceClass = class'AliasService'.default.weaponAliasesSource; - if (sourceClass == none) - { - _.logger.Auto(noWeaponAliasSource); - return none; + if (weaponAliasSource != none) { + weaponAliasSource.NewRef(); } - weaponSource = AliasSource(sourceClass.static.GetInstance(true)); - if (weaponSource == none) - { - _.logger.Auto(invalidWeaponAliasSource).ArgClass(sourceClass); - return none; - } - return weaponSource; + return weaponAliasSource; } /** - * Returns `AliasSource` that is designated in configuration files as + * Returns `BaseAliasSource` 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. + * @return Reference to the `BaseAliasSource` 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() +public final function BaseAliasSource GetColorSource() { - local AliasSource colorSource; - local class sourceClass; - sourceClass = class'AliasService'.default.colorAliasesSource; - if (sourceClass == none) - { - _.logger.Auto(noColorAliasSource); - return none; + if (colorAliasSource != none) { + colorAliasSource.NewRef(); } - colorSource = AliasSource(sourceClass.static.GetInstance(true)); - if (colorSource == none) - { - _.logger.Auto(invalidColorAliasSource).ArgClass(sourceClass); - return none; - } - return colorSource; + return colorAliasSource; } /** - * Returns `AliasSource` that is designated in configuration files as + * Returns `BaseAliasSource` that is designated in configuration files as * a source for feature aliases. * - * NOTE: while by default feature aliases source will contain only feature - * 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 feature aliases. + * @return Reference to the `BaseAliasSource` that contains feature aliases. * Can return `none` if no source for features was configured or * the configured source is incorrectly defined. */ -public final function AliasSource GetFeatureSource() +public final function BaseAliasSource GetFeatureSource() { - local AliasSource featureSource; - local class sourceClass; - sourceClass = class'AliasService'.default.featureAliasesSource; - if (sourceClass == none) - { - _.logger.Auto(noFeatureAliasSource); - return none; + if (featureAliasSource != none) { + featureAliasSource.NewRef(); } - featureSource = AliasSource(sourceClass.static.GetInstance(true)); - if (featureSource == none) - { - _.logger.Auto(invalidFeatureAliasSource).ArgClass(sourceClass); - return none; - } - return featureSource; + return featureAliasSource; } /** - * Returns `AliasSource` that is designated in configuration files as + * Returns `BaseAliasSource` that is designated in configuration files as * a source for entity aliases. * - * NOTE: while by default entity aliases source will contain only entity - * 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 entity aliases. + * @return Reference to the `BaseAliasSource` that contains entity aliases. * Can return `none` if no source for entities was configured or * the configured source is incorrectly defined. */ -public final function AliasSource GetEntitySource() +public final function BaseAliasSource GetEntitySource() { - local AliasSource entitySource; - local class sourceClass; - sourceClass = class'AliasService'.default.entityAliasesSource; - if (sourceClass == none) - { - _.logger.Auto(noEntityAliasSource); - return none; + if (entityAliasSource != none) { + entityAliasSource.NewRef(); } - entitySource = AliasSource(sourceClass.static.GetInstance(true)); - if (entitySource == none) - { - _.logger.Auto(invalidEntityAliasSource).ArgClass(sourceClass); + return entityAliasSource; +} + +private final function Text ResolveWithSource( + BaseText alias, + BaseAliasSource source, + optional bool copyOnFailure) +{ + local Text result; + local Text trimmedAlias; + + if (alias == none) { return none; } - return entitySource; + if (alias.StartsWith(P("$"))) { + trimmedAlias = alias.Copy(1); + } + else { + trimmedAlias = alias.Copy(); + } + if (source != none) { + result = source.Resolve(trimmedAlias, copyOnFailure); + } + else if (copyOnFailure) { + result = trimmedAlias.Copy(); + } + trimmedAlias.FreeSelf(); + return result; } /** - * Tries to look up a value stored for given alias in an `AliasSource` - * configured to store weapon aliases. Returns `none` on failure. + * Tries to look up a value stored for given alias in an `BaseAliasSource` + * configured to store weapon aliases. + * + * In Acedia aliases are typically prefixed with '$' to indicate that user + * means to enter alias. This method is able to handle both aliases with and + * without that prefix. This does not lead to conflicts, because '$' is cannot + * be a valid part of any alias. * * Lookup of alias can fail if either alias does not exist in weapon alias * source or weapon alias source itself does not exist @@ -179,7 +194,8 @@ public final function AliasSource GetEntitySource() * look up a value. Case-insensitive. * @param copyOnFailure Whether method should return copy of original * `alias` value in case caller source did not have any records - * corresponding to `alias`. + * corresponding to `alias`. If `alias` was specified with '$' prefix - + * it will be discarded. * @return If look up was successful - value, associated with the given * alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure` * flag: `copyOnFailure == false` means method will return `none` @@ -190,17 +206,17 @@ public final function Text ResolveWeapon( BaseText alias, optional bool copyOnFailure) { - local AliasSource source; - source = GetWeaponSource(); - if (source != none) { - return source.Resolve(alias, copyOnFailure); - } - return none; + return ResolveWithSource(alias, weaponAliasSource, copyOnFailure); } /** - * Tries to look up a value stored for given alias in an `AliasSource` - * configured to store color aliases. Reports error on failure. + * Tries to look up a value stored for given alias in an `BaseAliasSource` + * configured to store color aliases. + * + * In Acedia aliases are typically prefixed with '$' to indicate that user + * means to enter alias. This method is able to handle both aliases with and + * without that prefix. This does not lead to conflicts, because '$' is cannot + * be a valid part of any alias. * * Lookup of alias can fail if either alias does not exist in color alias * source or color alias source itself does not exist @@ -212,7 +228,8 @@ public final function Text ResolveWeapon( * look up a value. Case-insensitive. * @param copyOnFailure Whether method should return copy of original * `alias` value in case caller source did not have any records - * corresponding to `alias`. + * corresponding to `alias`. If `alias` was specified with '$' prefix - + * it will be discarded. * @return If look up was successful - value, associated with the given * alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure` * flag: `copyOnFailure == false` means method will return `none` @@ -223,17 +240,17 @@ public final function Text ResolveColor( BaseText alias, optional bool copyOnFailure) { - local AliasSource source; - source = GetColorSource(); - if (source != none) { - return source.Resolve(alias, copyOnFailure); - } - return none; + return ResolveWithSource(alias, colorAliasSource, copyOnFailure); } /** - * Tries to look up a value stored for given alias in an `AliasSource` - * configured to store feature aliases. Reports error on failure. + * Tries to look up a value stored for given alias in an `BaseAliasSource` + * configured to store feature aliases. + * + * In Acedia aliases are typically prefixed with '$' to indicate that user + * means to enter alias. This method is able to handle both aliases with and + * without that prefix. This does not lead to conflicts, because '$' is cannot + * be a valid part of any alias. * * Lookup of alias can fail if either alias does not exist in feature alias * source or feature alias source itself does not exist @@ -245,7 +262,8 @@ public final function Text ResolveColor( * look up a value. Case-insensitive. * @param copyOnFailure Whether method should return copy of original * `alias` value in case caller source did not have any records - * corresponding to `alias`. + * corresponding to `alias`. If `alias` was specified with '$' prefix - + * it will be discarded. * @return If look up was successful - value, associated with the given * alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure` * flag: `copyOnFailure == false` means method will return `none` @@ -256,17 +274,17 @@ public final function Text ResolveFeature( BaseText alias, optional bool copyOnFailure) { - local AliasSource source; - source = GetFeatureSource(); - if (source != none) { - return source.Resolve(alias, copyOnFailure); - } - return none; + return ResolveWithSource(alias, featureAliasSource, copyOnFailure); } /** - * Tries to look up a value stored for given alias in an `AliasSource` - * configured to store entity aliases. Reports error on failure. + * Tries to look up a value stored for given alias in an `BaseAliasSource` + * configured to store entity aliases. + * + * In Acedia aliases are typically prefixed with '$' to indicate that user + * means to enter alias. This method is able to handle both aliases with and + * without that prefix. This does not lead to conflicts, because '$' is cannot + * be a valid part of any alias. * * Lookup of alias can fail if either alias does not exist in entity alias * source or entity alias source itself does not exist @@ -278,7 +296,8 @@ public final function Text ResolveFeature( * look up a value. Case-insensitive. * @param copyOnFailure Whether method should return copy of original * `alias` value in case caller source did not have any records - * corresponding to `alias`. + * corresponding to `alias`. If `alias` was specified with '$' prefix - + * it will be discarded. * @return If look up was successful - value, associated with the given * alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure` * flag: `copyOnFailure == false` means method will return `none` @@ -289,23 +308,53 @@ public final function Text ResolveEntity( BaseText alias, optional bool copyOnFailure) { - local AliasSource source; - source = GetEntitySource(); - if (source != none) { - return source.Resolve(alias, copyOnFailure); + return ResolveWithSource(alias, entityAliasSource, copyOnFailure); +} + +/** + * Tries to look up a value stored for given alias in a custom alias source + * with a given name `sourceName`. + * + * In Acedia aliases are typically prefixed with '$' to indicate that user + * means to enter alias. This method is able to handle both aliases with and + * without that prefix. This does not lead to conflicts, because '$' is cannot + * be a valid part of any alias. + * + * Custom alias sources are any type of alias source that isn't built-in into + * Acedia. They can either be added manually by the admin through config file. + * + * Lookup of alias can fail if either alias does not exist in entity alias + * source or entity alias source itself does not exist + * (due to either faulty configuration or incorrect definition). + * To determine if entity alias source exists you can check + * `_.alias.GetCustomSource()` value. + * + * @param alias Alias, for which method will attempt to + * look up a value. Case-insensitive. + * @param copyOnFailure Whether method should return copy of original + * `alias` value in case caller source did not have any records + * corresponding to `alias`. If `alias` was specified with '$' prefix - + * it will be discarded. + * @return If look up was successful - value, associated with the given + * alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure` + * flag: `copyOnFailure == false` means method will return `none` + * and `copyOnFailure == true` means method will return `alias.Copy()`. + * If `alias == none` method always returns `none`. + */ +public final function Text ResolveCustom( + BaseText sourceName, + BaseText alias, + optional bool copyOnFailure) +{ + local BaseAliasSource customSource; + + customSource = GetCustomSource(sourceName); + if (customSource == none) { + return none; } - return none; + return ResolveWithSource(alias, customSource, copyOnFailure); } defaultproperties { - // TODO: all this shit below can be done as two messages - noWeaponAliasSource = (l=LOG_Error,m="No weapon aliases source configured for Acedia's alias API. Error is most likely cause by erroneous config.") - invalidWeaponAliasSource = (l=LOG_Error,m="`AliasSource` class `%1` 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`.") - noColorAliasSource = (l=LOG_Error,m="No color aliases source configured for Acedia's alias API. Error is most likely cause by erroneous config.") - invalidColorAliasSource = (l=LOG_Error,m="`AliasSource` class `%1` 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`.") - noFeatureAliasSource = (l=LOG_Error,m="No feature aliases source configured for Acedia's alias API. Error is most likely cause by erroneous config.") - invalidFeatureAliasSource = (l=LOG_Error,m="`AliasSource` class `%1` is configured to store feature 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`.") - noEntityAliasSource = (l=LOG_Error,m="No entity aliases source configured for Acedia's alias API. Error is most likely cause by erroneous config.") - invalidEntityAliasSource = (l=LOG_Error,m="`AliasSource` class `%1` is configured to store entity 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`.") } \ No newline at end of file diff --git a/sources/Aliases/AliasesStorage.uc b/sources/Aliases/AliasesStorage.uc new file mode 100644 index 0000000..791064f --- /dev/null +++ b/sources/Aliases/AliasesStorage.uc @@ -0,0 +1,205 @@ +/** + * 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 `AliasesStorage` object can store several aliases for a single + * value. + * It is recommended that you do not try to access these objects directly. + * `AliasesStorage` is abstract, so it can never be created, for any + * actual use create a child class. Suggested name scheme is + * "Aliases", like "ColorAliases" or "WeaponAliases". + * 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). + * [INTERNAL] This is an internal object and should not be directly modified, + * its only allowed use is documented way to create new alias sources. + * Copyright 2019-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 AliasesStorage extends AcediaObject + perObjectConfig + config(AcediaAliases) + abstract; + +// Aliases, recorded by this `AliasesStorage` object that all refer to +// the same value, defined by this object's name `string(self.name)` +var protected config array alias; + +// Name of the configurational file (without extension) where +// this `AliasSource`'s data will be stored +var protected const string configName; + +// Link to the `AliasSource` that uses `AliasesStorage` objects of this class. +// To ensure that any `AliasesStorage` sub-class only belongs to one +// `AliasSource`. +var public const class sourceClass; + +// `true` means that this `AliasesStorage` object is awaiting saving into its +// config +var private bool pendingSaveToConfig; + +// Since: +// 1. '.'s in values are converted into ':' for storage purposes; +// 2. We have to store values in `string` to make use of config files. +// we need methods to convert between "storage" (`string`) +// and "actual" (`Text`) value version. +// `ToStorageVersion()` and `ToActualVersion()` do that. +private final static function string ToStorageVersion(BaseText actualValue) +{ + return Repl(actualValue.ToString(), ".", ":"); +} + +// See comment to `ToStorageVersion()` +private final static function Text ToActualVersion(string storageValue) +{ + return __().text.FromString(Repl(storageValue, ":", ".")); +} + +/** + * Loads all `AliasesStorage` objects from their config file + * (defined in paired `AliasSource` class). + * + * This is an internal method and returned `AliasesStorage` objects require + * a special treatment: they aren't meant to be used with Acedia's reference + * counting - never release their references. + * + * @return Array of all `Aliases` objects, loaded from their config file. + */ +public static final function array LoadAllObjects() +{ + local int i; + local array objectNames; + local array loadedAliasObjects; + + objectNames = GetPerObjectNames(default.configName, + string(default.class.name), MaxInt); + for (i = 0; i < objectNames.length; i += 1) { + loadedAliasObjects[i] = LoadObjectByName(objectNames[i]); + } + return loadedAliasObjects; +} + +// Loads a new `AliasesStorage` object by it's given name (`objectName`). +private static final function AliasesStorage LoadObjectByName( + string objectName) +{ + local AliasesStorage result; + + // Since `MemoryAPI` for now does not support specifying names + // to created objects - do some manual dark magic and + // initialize this shit ourselves. Don't repeat this at home, kids. + result = new(none, objectName) default.class; + result._constructor(); + return result; +} + +/** + * Loads a new `AliasesStorage` object based on the value (`aliasesValue`) + * of it's aliases. + * + * @param aliasesValue Value that aliases in this `Aliases` object will + * correspond to. + * @return Instance of `AliasesStorage` object with a given name. + */ +public static final function AliasesStorage LoadObject(BaseText aliasesValue) +{ + if (aliasesValue != none) { + return LoadObjectByName(ToStorageVersion(aliasesValue)); + } + return none; +} + +/** + * Returns value that caller's `Aliases` object's aliases point to. + * + * @return Value, stored by this object. + */ +public final function Text GetValue() +{ + return ToActualVersion(string(self.name)); +} + +/** + * Returns array of aliases that caller `AliasesStorage` tells us point to + * it's value. + * + * @return Array of all aliases, stored by caller `AliasesStorage` object. + */ +public final function array GetAliases() +{ + local int i; + local array textAliases; + + for (i = 0; i < alias.length; i += 1) { + textAliases[i] = _.text.FromString(alias[i]); + } + return textAliases; +} + +/** + * [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 `AliasesStorage` object. + * Expected to be in lower case. + */ +public final function RemoveAlias_S(string aliasToRemove) +{ + local int i; + local bool removedAlias; + + while (i < alias.length) + { + if (aliasToRemove ~= alias[i]) + { + alias.Remove(i, 1); + removedAlias = true; + } + else { + i += 1; + } + } + if (!pendingSaveToConfig) + { + pendingSaveToConfig = true; + _.scheduler.RequestDiskAccess(self).connect = SaveOrClear; + } +} + +/** + * 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. + */ +private final function SaveOrClear() +{ + if (alias.length <= 0) { + ClearConfig(); + } + else { + SaveConfig(); + } + pendingSaveToConfig = false; +} + +defaultproperties +{ + sourceClass = class'AliasSource' + configName = "AcediaAliases" +} \ No newline at end of file diff --git a/sources/Aliases/Aliases_Feature.uc b/sources/Aliases/Aliases_Feature.uc new file mode 100644 index 0000000..f1e8223 --- /dev/null +++ b/sources/Aliases/Aliases_Feature.uc @@ -0,0 +1,249 @@ +/** + * This feature provides a mechanism to define commands that automatically + * parse their arguments into standard Acedia collection. It also allows to + * manage them (and specify limitation on how they can be called) in a + * centralized manner. + * Copyright 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 Aliases_Feature extends Feature + dependson(Aliases); + +struct ClassSourcePair +{ + var class class; + var AliasSource source; +}; +// We don't want to reload `AliasSource`s several times when this feature is +// disabled and re-enabled or its config is swapped, so we keep all loaded +// sources here at all times and look them up from here +var private array loadedSources; + +// Loaded `AliasSource`s +var private AliasSource weaponAliasSource; +var private AliasSource colorAliasSource; +var private AliasSource featureAliasSource; +var private AliasSource entityAliasSource; +// Everything else +var private HashTable customSources; + +var private LoggerAPI.Definition errCannotLoadAliasSource, errEmptyName; +var private LoggerAPI.Definition errDuplicateCustomSource; + +protected function OnEnabled() +{ + _.alias._reloadSources(); +} + +protected function OnDisabled() +{ + DropSources(); + _.alias._reloadSources(); +} + +private function DropSources() +{ + _.memory.Free(weaponAliasSource); + weaponAliasSource = none; + _.memory.Free(colorAliasSource); + colorAliasSource = none; + _.memory.Free(featureAliasSource); + featureAliasSource = none; + _.memory.Free(entityAliasSource); + entityAliasSource = none; + _.memory.Free(customSources); + customSources = none; +} + +// Create each `AliasSource` instance only once to avoid any possible +// nonsense with loading named objects several times: alias sources don't use +// `AcediaConfig`s, so they don't automatically avoid named config reloading +private function AliasSource GetSource(class sourceClass) +{ + local int i; + local AliasSource newSource; + local ClassSourcePair newPair; + + if (sourceClass == none) { + return none; + } + for (i = 0; i < loadedSources.length; i += 1) + { + if (loadedSources[i].class == sourceClass) { + return loadedSources[i].source; + } + } + newSource = AliasSource(_.memory.Allocate(sourceClass)); + if (newSource != none) + { + newPair.class = sourceClass; + newPair.source = newSource; + // One reference we store, one we return + newSource.NewRef(); + } + else { + _.logger.Auto(errCannotLoadAliasSource).ArgClass(sourceClass); + } + return newSource; +} + +protected function SwapConfig(FeatureConfig config) +{ + local Aliases newConfig; + + newConfig = Aliases(config); + if (newConfig == none) { + return; + } + _.memory.Free(weaponAliasSource); + DropSources(); + weaponAliasSource = GetSource(newConfig.weaponAliasSource); + colorAliasSource = GetSource(newConfig.colorAliasSource); + featureAliasSource = GetSource(newConfig.featureAliasSource); + entityAliasSource = GetSource(newConfig.entityAliasSource); + LoadCustomSources(newConfig.customSource); + _.alias._reloadSources(); +} + +private function LoadCustomSources( + array configCustomSources) +{ + local int i; + local bool reportedEmptyName; + local Text nextKey; + local AliasSource nextSource, conflictingSource; + + _.memory.Free(customSources); + customSources = _.collections.EmptyHashTable(); + for (i = 0; i < configCustomSources.length; i += 1) + { + if (configCustomSources[i].name == "" && !reportedEmptyName) + { + reportedEmptyName = true; + _.logger.Auto(errEmptyName); + } + nextKey = _.text.FromString(configCustomSources[i].name); + // We only store `AliasSource`s + conflictingSource = AliasSource(customSources.GetItem(nextKey)); + if (conflictingSource != none) + { + _.logger.Auto(errDuplicateCustomSource) + .ArgClass(conflictingSource.class) + .Arg(nextKey) // Releases `nextKey` + .ArgClass(configCustomSources[i].source); + conflictingSource.FreeSelf(); + continue; + } + nextSource = GetSource(configCustomSources[i].source); + if (nextSource != none) + { + customSources.SetItem(nextKey, nextSource); + nextSource.FreeSelf(); + } + nextKey.FreeSelf(); + } +} + +/** + * Returns `AliasSource` for weapon aliases. + * + * @return `AliasSource`, configured to store weapon aliases. + */ +public function AliasSource GetWeaponSource() +{ + if (weaponAliasSource != none) { + weaponAliasSource.Newref(); + } + return weaponAliasSource; +} + +/** + * Returns `AliasSource` for color aliases. + * + * @return `AliasSource`, configured to store color aliases. + */ +public function AliasSource GetColorSource() +{ + if (colorAliasSource != none) { + colorAliasSource.Newref(); + } + return colorAliasSource; +} + +/** + * Returns `AliasSource` for feature aliases. + * + * @return `AliasSource`, configured to store feature aliases. + */ +public function AliasSource GetFeatureSource() +{ + if (featureAliasSource != none) { + featureAliasSource.Newref(); + } + return featureAliasSource; +} + +/** + * Returns `AliasSource` for entity aliases. + * + * @return `AliasSource`, configured to store entity aliases. + */ +public function AliasSource GetEntitySource() +{ + if (entityAliasSource != none) { + entityAliasSource.Newref(); + } + return entityAliasSource; +} + +/** + * Returns custom `AliasSource` with a given name. + * + * @return Custom `AliasSource`, configured with a given name `sourceName`. + */ +public function AliasSource GetCustomSource(BaseText sourceName) +{ + if (sourceName == none) { + return none; + } + // We only store `AliasSource`s + return AliasSource(customSources.GetItem(sourceName)); +} + +/** + * Returns custom `AliasSource` with a given name `sourceName. + * + * @return Custom `AliasSource`, configured with a given name `sourceName`. + */ +public function AliasSource GetCustomSource_S(string sourceName) +{ + local Text wrapper; + local AliasSource result; + + wrapper = _.text.FromString(sourceName); + result = GetCustomSource(wrapper); + wrapper.FreeSelf(); + return result; +} + +defaultproperties +{ + configClass = class'Aliases' + errEmptyName = (l=LOG_Error,m="Empty name provided for the custom alias source. This is likely due to an erroneous config.") + errCannotLoadAliasSource = (l=LOG_Error,m="Failed to load alias source class `%1`.") + errDuplicateCustomSource = (l=LOG_Error,m="Custom alias source `%1` is already registered with name '%2'. Alias source `%3` with the same name will be ignored.") +} \ No newline at end of file diff --git a/sources/Aliases/BaseAliasSource.uc b/sources/Aliases/BaseAliasSource.uc new file mode 100644 index 0000000..0244ce2 --- /dev/null +++ b/sources/Aliases/BaseAliasSource.uc @@ -0,0 +1,283 @@ +/** + * Aliases allow users to define human-readable and easier to use + * "synonyms" to some symbol sequences (mainly names of UnrealScript classes). + * This is an interface class that can be implemented in various different + * ways. + * Several `AliasSource`s are supposed to exist separately, each storing + * aliases of particular kind: for weapon, zeds, colors, etc.. + * Copyright 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 BaseAliasSource extends AcediaObject + abstract; + +/** + * # `AliasSource` + * + * Interface for any alias source instance that includes basic methods for + * alias lookup and adding/removal. + * + * ## Aliases + * + * Aliases in Acedia are usually defined as either `$`. + * `` can only contain ASCII latin letters and digits. + * `` is meant to define a human-readable name for something + * (e.g. "m14" for `KFMod.M14EBRBattleRifle`). + * Aliases are *case-insensitive*. + * '$' prefix is used to emphasize that what user is specifying is, + * in fact, an alias and it *is not* actually important for aliases feature + * and API: only `` is used. However, for convenience's sake, + * `AliasesAPI` usually recognizes aliases both with and without '$' prefix. + * Alias sources (classes derived from `BaseAliasSource`), however should only + * handle resolving ``, treating '$' prefix as a mistake in alias + * parameter. + * + * ## Implementation + * + * As far as implementation goes, it's up to you how your own child alias + * source class is configured to obtain its aliases, but `Aliases_Feature` + * should only create a single instance for every source class (although + * nothing can prevent other mods from creating more instances, so we cannot + * give any guarantees). + * Methods that add or remove aliases are allowed to fail for whatever + * reason is valid for your source's case (it might forbid adding aliases + * at all), as long as they return `false`. + * Although all built-in aliases are storing case-insensitive values, + * `BaseAliasSource` does not demand that and allows to configure this behavior + * via `AreValuesCaseSensitive()`. This is important for `GetAliases()` method + * that returns all aliases referring to a given value. + */ + +/** + * Returns whether caller alias source class stores case-sensitive values. + * + * This information is necessary for organizing lookup of aliases by + * a given value. Aliases themselves are always case-insensitive. + * + * This method should not change returned value for any fixed class. + * + * @return `true` if stored values are case-sensitive and `false` if they are + * case-insensitive. + */ +public static function bool AreValuesCaseSensitive(); + +/** + * Returns all aliases that represent given value `value`. + * + * @param value Value for which to return all its aliases. + * Whether it is treated as case-sensitive is decided by + * `AreValuesCaseSensitive()` method, but all default alias sources are + * case-insensitive. + * @return Array of all aliases that refer to the given `value` inside + * the caller alias source. All `Text` references are guaranteed to not be + * `none` or duplicated. + */ +public function array GetAliases(BaseText value); + +/** + * Returns all aliases that represent given value `value`. + * + * @param value Value for which to return all its aliases. + * Whether it is treated as case-sensitive is decided by + * `AreValuesCaseSensitive()` method, but all default alias sources are + * case-insensitive. + * @return Array of all aliases that refer to the given `value` inside + * the caller alias source. All `string` references are guaranteed to not + * be duplicated. + */ +public function array GetAliases_S(string value) +{ + local int i; + local Text valueAsText; + local array resultWithTexts; + local array result; + + valueAsText = _.text.FromString(value); + resultWithTexts = GetAliases(valueAsText); + _.memory.Free(valueAsText); + for (i = 0; i < resultWithTexts.length; i += 1) { + result[result.length] = resultWithTexts[i].ToString(); + } + _.memory.FreeMany(resultWithTexts); + return result; +} + +/** + * Checks if given alias is present in caller `AliasSource`. + * + * NOTE: having '$' prefix is considered to be invalid for `alias` by this + * method. + * + * @param alias Alias to check, case-insensitive. + * @return `true` if present, `false` otherwise. + */ +public function bool HasAlias(BaseText alias); + +/** + * Checks if given alias is present in caller `AliasSource`. + * + * NOTE: having '$' prefix is considered to be invalid for `alias` by this + * method. + * + * @param alias Alias to check, case-insensitive. + * @return `true` if present, `false` otherwise. + */ +public function bool HasAlias_S(string alias) +{ + local bool result; + local Text aliasAsText; + + aliasAsText = _.text.FromString(alias); + result = HasAlias(aliasAsText); + _.memory.Free(aliasAsText); + return result; +} + +/** + * Returns value stored for the given alias in caller `AliasSource` + * (as well as it's `Aliases` objects). + * + * NOTE: having '$' prefix is considered to be invalid for `alias` by this + * method. + * + * @param alias Alias, for which method will attempt to return + * a value. Case-insensitive. If given `alias` starts with "$" character - + * that character will be removed before resolving that alias. + * @param copyOnFailure Whether method should return copy of original + * `alias` value in case caller source did not have any records + * corresponding to `alias`. + * @return If look up was successful - value, associated with the given + * alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure` + * flag: `copyOnFailure == false` means method will return `none` + * and `copyOnFailure == true` means method will return `alias.Copy()`. + * If `alias == none` method always returns `none`. + */ +public function Text Resolve(BaseText alias, optional bool copyOnFailure); + +/** + * Returns value stored for the given alias in caller `AliasSource` + * (as well as it's `Aliases` objects). + * + * NOTE: having '$' prefix is considered to be invalid for `alias` by this + * method. + * + * @param alias Alias, for which method will attempt to return + * a value. Case-insensitive. If given `alias` starts with "$" character - + * that character will be removed before resolving that alias. + * @param copyOnFailure Whether method should return copy of original + * `alias` value in case caller source did not have any records + * corresponding to `alias`. + * @return If look up was successful - value, associated with the given + * alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure` + * flag: `copyOnFailure == false` means method will return empty `string` + * and `copyOnFailure == true` means method will return `alias`. + */ +public function string Resolve_S( + string alias, + optional bool copyOnFailure) +{ + local Text resultAsText; + local Text aliasAsText; + + aliasAsText = _.text.FromString(alias); + resultAsText = Resolve(aliasAsText, copyOnFailure); + return _.text.IntoString(resultAsText); +} + +/** + * 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 or `aliasValue == none`. + * + * NOTE: having '$' prefix is considered to be invalid for `alias` by this + * method. + * + * @param aliasToAdd Alias that you want to add to caller source. + * Alias names are case-insensitive. + * @param aliasValue Intended value of this alias. + * @return `true` if alias was added and `false` otherwise (alias was invalid). + */ +public function bool AddAlias(BaseText aliasToAdd, BaseText aliasValue); + +/** + * 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. + * + * NOTE: having '$' prefix is considered to be invalid for `alias` by this + * method. + * + * @param aliasToAdd Alias that you want to add to caller source. + * Alias names are case-insensitive. + * @param aliasValue Intended value of this alias. + * @return `true` if alias was added and `false` otherwise (alias was invalid). + */ +public function bool AddAlias_S(string aliasToAdd, string aliasValue) +{ + local bool result; + local Text aliasAsText, valueAsText; + + aliasAsText = _.text.FromString(aliasToAdd); + valueAsText = _.text.FromString(aliasValue); + result = AddAlias(aliasAsText, valueAsText); + _.memory.Free(aliasAsText); + _.memory.Free(valueAsText); + return result; +} + +/** + * Removes alias (all records with it, in case of duplicates) from + * the caller `AliasSource`. + * + * NOTE: having '$' prefix is considered to be invalid for `alias` by this + * method. + * + * @param aliasToRemove Alias that you want to remove from caller source. + * @return `true` if an alias was present in the source and was deleted and + * `false` if there was no specified alias in the first place. + */ +public function bool RemoveAlias(BaseText aliasToRemove); + +/** + * Removes alias (all records with it, in case of duplicates) from + * the caller `AliasSource`. + * + * NOTE: having '$' prefix is considered to be invalid for `alias` by this + * method. + * + * @param aliasToRemove Alias that you want to remove from caller source. + * @return `true` if an alias was present in the source and was deleted and + * `false` if there was no specified alias in the first place. + */ +public function bool RemoveAlias_S(string aliasToRemove) +{ + local bool result; + local Text aliasAsText; + + aliasAsText = _.text.FromString(aliasToRemove); + result = RemoveAlias(aliasAsText); + _.memory.Free(aliasAsText); + return result; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Aliases/BuiltInSources/ColorAliases.uc b/sources/Aliases/BuiltInSources/ColorAliases.uc index 9a00b78..391e6a5 100644 --- a/sources/Aliases/BuiltInSources/ColorAliases.uc +++ b/sources/Aliases/BuiltInSources/ColorAliases.uc @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class ColorAliases extends Aliases +class ColorAliases extends AliasesStorage perObjectConfig config(AcediaAliases_Colors); diff --git a/sources/Aliases/BuiltInSources/EntityAliases.uc b/sources/Aliases/BuiltInSources/EntityAliases.uc index 3b41559..69be1f8 100644 --- a/sources/Aliases/BuiltInSources/EntityAliases.uc +++ b/sources/Aliases/BuiltInSources/EntityAliases.uc @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class EntityAliases extends Aliases +class EntityAliases extends AliasesStorage perObjectConfig config(AcediaAliases_Entities); diff --git a/sources/Aliases/BuiltInSources/FeatureAliasSource.uc b/sources/Aliases/BuiltInSources/FeatureAliasSource.uc index 9167b6d..e58bfe8 100644 --- a/sources/Aliases/BuiltInSources/FeatureAliasSource.uc +++ b/sources/Aliases/BuiltInSources/FeatureAliasSource.uc @@ -18,7 +18,7 @@ * along with Acedia. If not, see . */ class FeatureAliasSource extends AliasSource - config(AcediaAliases); + config(AcediaAliases_Features); defaultproperties { diff --git a/sources/Aliases/BuiltInSources/FeatureAliases.uc b/sources/Aliases/BuiltInSources/FeatureAliases.uc index 7acc0b6..2cfed3b 100644 --- a/sources/Aliases/BuiltInSources/FeatureAliases.uc +++ b/sources/Aliases/BuiltInSources/FeatureAliases.uc @@ -17,9 +17,9 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class FeatureAliases extends Aliases +class FeatureAliases extends AliasesStorage perObjectConfig - config(AcediaAliases); + config(AcediaAliases_Features); defaultproperties { diff --git a/sources/Aliases/BuiltInSources/WeaponAliases.uc b/sources/Aliases/BuiltInSources/WeaponAliases.uc index 03f84d3..85d81ed 100644 --- a/sources/Aliases/BuiltInSources/WeaponAliases.uc +++ b/sources/Aliases/BuiltInSources/WeaponAliases.uc @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class WeaponAliases extends Aliases +class WeaponAliases extends AliasesStorage perObjectConfig config(AcediaAliases_Weapons); diff --git a/sources/Aliases/Tests/MockAliases.uc b/sources/Aliases/Tests/MockAliases.uc index dcc73e6..ef9d71c 100644 --- a/sources/Aliases/Tests/MockAliases.uc +++ b/sources/Aliases/Tests/MockAliases.uc @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class MockAliases extends Aliases +class MockAliases extends AliasesStorage perObjectConfig config(AcediaAliases_Tests); diff --git a/sources/Aliases/Tests/TEST_Aliases.uc b/sources/Aliases/Tests/TEST_Aliases.uc index 82700a0..213b5ef 100644 --- a/sources/Aliases/Tests/TEST_Aliases.uc +++ b/sources/Aliases/Tests/TEST_Aliases.uc @@ -23,15 +23,17 @@ class TEST_Aliases extends TestCase protected static function TESTS() { Context("Testing loading aliases from a mock object `MockAliasSource`."); + Issue("`GetCustomSource()` fails to return alias source."); + TEST_ExpectNotNone(__().alias.GetCustomSource(P("mock"))); SubTest_AliasLoadingCorrect(); SubTest_AliasLoadingIncorrect(); } protected static function SubTest_AliasLoadingCorrect() { - local AliasSource source; + local BaseAliasSource source; Issue("`Resolve()` fails to return alias value that should be loaded."); - source = __().alias.GetCustomSource(class'MockAliasSource'); + source = __().alias.GetCustomSource(P("mock")); TEST_ExpectTrue(source.Resolve(P("Global")).ToString() == "value"); TEST_ExpectTrue(source.Resolve(P("ford")).ToString() == "car"); @@ -51,21 +53,19 @@ protected static function SubTest_AliasLoadingCorrect() TEST_ExpectTrue( source.Resolve(P("HardToBeAGod")).ToString() == "sci.fi"); - Issue("Aliases with empty values in alias name or their value are handled" - @ "incorrectly."); - TEST_ExpectTrue(source.Resolve(P("")).ToString() == "empty"); + Issue("Aliases corresponding to empty values are handled incorrectly."); TEST_ExpectTrue(source.Resolve(P("also")).ToString() == ""); } protected static function SubTest_AliasLoadingIncorrect() { - local AliasSource source; + local BaseAliasSource source; Issue("`AliasAPI` cannot return value custom source."); - source = __().alias.GetCustomSource(class'MockAliasSource'); + source = __().alias.GetCustomSource(P("mock")); TEST_ExpectNotNone(source); Issue("`Resolve()` reports success of finding inexistent alias."); - source = __().alias.GetCustomSource(class'MockAliasSource'); + source = __().alias.GetCustomSource(P("mock")); TEST_ExpectNone(source.Resolve(P("noSuchThing"))); Issue("`HasAlias()` reports inexistent alias as present."); diff --git a/sources/BaseRealm/AcediaEnvironment/AcediaEnvironment.uc b/sources/BaseRealm/AcediaEnvironment/AcediaEnvironment.uc index 4a0269b..0eaf13c 100644 --- a/sources/BaseRealm/AcediaEnvironment/AcediaEnvironment.uc +++ b/sources/BaseRealm/AcediaEnvironment/AcediaEnvironment.uc @@ -23,7 +23,7 @@ class AcediaEnvironment extends AcediaObject; * # `AcediaEnvironment` * * Instance of this class will be used by Acedia to manage resources available - * from different packages like `Feature`s, aliases, etc.. + * from different packages like `Feature`s and such other etc.. * This is mostly necessary to implement Acedia loader (and, possibly, * its alternatives) that would load available packages and enable `Feature`s * admin wants to be enabled. @@ -244,13 +244,6 @@ private final function ReadManifest(class<_manifest> manifestClass) { local int i; - for (i = 0; i < manifestClass.default.aliasSources.length; i += 1) - { - if (manifestClass.default.aliasSources[i] == none) { - continue; - } - _.memory.Allocate(manifestClass.default.aliasSources[i]); - } for (i = 0; i < manifestClass.default.features.length; i += 1) { if (manifestClass.default.features[i] == none) { diff --git a/sources/Commands/Aliases/CommandAliasSource.uc b/sources/Commands/Aliases/CommandAliasSource.uc index 745452f..c37c57f 100644 --- a/sources/Commands/Aliases/CommandAliasSource.uc +++ b/sources/Commands/Aliases/CommandAliasSource.uc @@ -17,7 +17,8 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class CommandAliasSource extends AliasSource; +class CommandAliasSource extends AliasSource + config(AcediaAliases_Commands); defaultproperties { diff --git a/sources/Commands/Aliases/CommandAliases.uc b/sources/Commands/Aliases/CommandAliases.uc index 3faaf93..4ef02ba 100644 --- a/sources/Commands/Aliases/CommandAliases.uc +++ b/sources/Commands/Aliases/CommandAliases.uc @@ -17,8 +17,9 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class CommandAliases extends Aliases - perObjectConfig; +class CommandAliases extends AliasesStorage + perObjectConfig + config(AcediaAliases_Commands); defaultproperties { diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 9c43224..581c733 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -22,13 +22,9 @@ defaultproperties { - features(0) = class'Commands_Feature' - features(1) = class'Avarice_Feature' - aliasSources(0) = class'AliasSource' - aliasSources(1) = class'WeaponAliasSource' - aliasSources(2) = class'ColorAliasSource' - aliasSources(3) = class'FeatureAliasSource' - aliasSources(4) = class'EntityAliasSource' + features(0) = class'Aliases_Feature' + features(1) = class'Commands_Feature' + features(2) = class'Avarice_Feature' testCases(0) = class'TEST_Base' testCases(1) = class'TEST_ActorService' testCases(2) = class'TEST_Boxes'