diff --git a/sources/BaseAPI/API/Commands/Tools/CmdItemsTool.uc b/sources/BaseAPI/API/Commands/Tools/CmdItemsTool.uc
new file mode 100644
index 0000000..e441249
--- /dev/null
+++ b/sources/BaseAPI/API/Commands/Tools/CmdItemsTool.uc
@@ -0,0 +1,306 @@
+/**
+ * Author: dkanus
+ * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
+ * License: GPL
+ * Copyright 2023 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 CmdItemsTool extends AcediaObject
+ dependson(CommandAPI)
+ abstract;
+
+//! This is a base class for auxiliary objects that will be used for storing
+//! named [`Command`] instances and [`Voting`] classes: they both have in common
+//! the need to remember who was authorized to use them (i.e. which user group)
+//! and with what permissions (i.e. name of the config that contains appropriate
+//! permissions).
+//!
+//! Aside from trivial accessors to its data, it also provides a way to resolve
+//! the best permissions available to the user by finding the most priviledged
+//! group he belongs to.
+//!
+//! NOTE: child classes must implement `MakeCard()` method and can override
+//! `DiscardCard()` method to catch events of removing items from storage.
+
+/// Allows to specify a base class requirement for this tool - only classes
+/// that were derived from it can be stored inside.
+var protected const class ruleBaseClass;
+
+/// Names of user groups that can decide permissions for items,
+/// in order of importance: from most significant to the least significant.
+/// This is used for resolving the best permissions for each user.
+var private array permissionGroupOrder;
+
+/// Maps item names to their [`ItemCards`] with information about which groups
+/// are authorized to use this particular item.
+var private HashTable registeredCards;
+
+var LoggerAPI.Definition errItemInvalidName;
+var LoggerAPI.Definition errItemDuplicate;
+
+protected function Constructor() {
+ registeredCards = _.collections.EmptyHashTable();
+}
+
+protected function Finalizer() {
+ _.memory.Free(registeredCards);
+ _.memory.FreeMany(permissionGroupOrder);
+ registeredCards = none;
+ permissionGroupOrder.length = 0;
+}
+
+/// Registers given item class under the specified (case-insensitive) name.
+///
+/// If name parameter is omitted (specified as `none`) or is an invalid name
+/// (according to [`BaseText::IsValidName()`] method), then item class will not
+/// be registered.
+///
+/// Returns `true` if item was successfully registered and `false` otherwise`.
+///
+/// # Errors
+///
+/// If provided name that is invalid or already taken by a different item -
+/// a warning will be logged and item class won't be registered.
+public function bool AddItemClass(class itemClass, BaseText itemName) {
+ local Text itemKey;
+ local ItemCard newCard, existingCard;
+
+ if (itemClass == none) return false;
+ if (itemName == none) return false;
+ if (registeredCards == none) return false;
+
+ if (ruleBaseClass == none || !ClassIsChildOf(itemClass, ruleBaseClass)) {
+ return false;
+ }
+ // The item name is transformed into lowercase, immutable value.
+ // This facilitates the use of item names as keys in a [`HashTable`],
+ // enabling case-insensitive matching.
+ itemKey = itemName.LowerCopy();
+ if (itemKey == none || !itemKey.IsValidName()) {
+ _.logger.Auto(errItemInvalidName).ArgClass(itemClass).Arg(itemKey);
+ return false;
+ }
+ // Guaranteed to only store cards
+ existingCard = ItemCard(registeredCards.GetItem(itemName));
+ if (existingCard != none) {
+ _.logger.Auto(errItemDuplicate)
+ .ArgClass(existingCard.GetItemClass())
+ .Arg(itemKey)
+ .ArgClass(itemClass);
+ _.memory.Free(existingCard);
+ return false;
+ }
+ newCard = MakeCard(itemClass, itemName);
+ registeredCards.SetItem(itemKey, newCard);
+ _.memory.Free2(itemKey, newCard);
+ return true;
+}
+
+/// Removes item of given class from the list of registered items.
+///
+/// Removing once registered item is not an action that is expected to
+/// be performed under normal circumstances and does not have an efficient
+/// implementation (it is linear on the current amount of items).
+///
+/// Returns `true` if successfully removed registered item class and
+/// `false` otherwise (either item wasn't registered or caller tool
+/// initialized).
+public function bool RemoveItemClass(class itemClass) {
+ local int i;
+ local CollectionIterator iter;
+ local ItemCard nextCard;
+ local array keysToRemove;
+
+ if (itemClass == none) return false;
+ if (registeredCards == none) return false;
+
+ // Removing items during iterator breaks an iterator, so first we find
+ // all the keys to remove
+ iter = registeredCards.Iterate();
+ iter.LeaveOnlyNotNone();
+ while (!iter.HasFinished()) {
+ // Guaranteed to only be `ItemCard`
+ nextCard = ItemCard(iter.Get());
+ if (nextCard.GetItemClass() == itemClass) {
+ keysToRemove[keysToRemove.length] = Text(iter.GetKey());
+ DiscardCard(nextCard);
+ }
+ _.memory.Free(nextCard);
+ iter.Next();
+ }
+ iter.FreeSelf();
+ // Actual clean up everything in `keysToRemove`
+ for (i = 0; i < keysToRemove.length; i += 1) {
+ registeredCards.RemoveItem(keysToRemove[i]);
+ }
+ _.memory.FreeMany(keysToRemove);
+ return (keysToRemove.length > 0);
+}
+
+/// Allows to specify the order of the user group in terms of privilege for
+/// accessing stored items. Only specified groups will be used when resolving
+/// appropriate permissions config name for a user.
+public final function SetPermissionGroupOrder(array groupOrder) {
+ local int i;
+
+ _.memory.FreeMany(permissionGroupOrder);
+ permissionGroupOrder.length = 0;
+ for (i = 0; i < groupOrder.length; i += 1) {
+ if (groupOrder[i] != none) {
+ permissionGroupOrder[permissionGroupOrder.length] = groupOrder[i].Copy();
+ }
+ }
+}
+
+/// Specifies what permissions (given by the config name) given user group has
+/// when using an item with a specified name.
+///
+/// Method must be called after item with a given name is added.
+///
+/// If this config name is specified as `none`, then "default" will be
+/// used instead. For non-`none` values, only an invalid name (according to
+/// [`BaseText::IsValidName()`] method) will prevent the group from being
+/// registered.
+///
+/// Method will return `true` if group was successfully authorized and `false`
+/// otherwise (either group already authorized or no item with specified name
+/// was added in the caller tool so far).
+///
+/// # Errors
+///
+/// If specified group was already authorized to use card's item, then it
+/// will log a warning message about it.
+public function bool AuthorizeUsage(BaseText itemName, BaseText groupName, BaseText configName) {
+ local bool result;
+ local ItemCard relevantCard;
+
+ if (configName != none && !configName.IsValidName()) {
+ return false;
+ }
+ relevantCard = GetCard(itemName);
+ if (relevantCard != none) {
+ result = relevantCard.AuthorizeGroupWithConfig(groupName, configName);
+ _.memory.Free(relevantCard);
+ }
+ return result;
+}
+
+/// Returns struct with item class (+ instance, if one was stored) for a given
+/// case in-sensitive item name and name of the config with best permissions
+/// available to the player with provided ID.
+///
+/// Function only returns `none` for item class if item with a given name
+/// wasn't found.
+/// Config name being `none` with non-`none` item class in the result means
+/// that user with provided ID doesn't have permissions for using the item at
+/// all.
+public final function CommandAPI.ItemConfigInfo ResolveItem(BaseText itemName, BaseText textID) {
+ local int i;
+ local ItemCard relevantCard;
+ local CommandAPI.ItemConfigInfo result;
+
+ relevantCard = GetCard(itemName);
+ if (relevantCard == none) {
+ // At this point contains `none` for all values -> indicates a failure
+ // to find item in storage
+ return result;
+ }
+ result.instance = relevantCard.GetItem();
+ result.class = relevantCard.GetItemClass();
+ if (textID == none) {
+ return result;
+ }
+ // Look through all `permissionGroupOrder` in order to find most priviledged
+ // group that user with `textID` belongs to
+ for (i = 0; i < permissionGroupOrder.length && result.configName == none; i += 1) {
+ if (_.users.IsSteamIDInGroup(textID, permissionGroupOrder[i])) {
+ result.configName = relevantCard.GetConfigNameForGroup(permissionGroupOrder[i]);
+ }
+ }
+ _.memory.Free(relevantCard);
+ return result;
+}
+
+/// Returns all item classes that are stored inside caller tool.
+///
+/// Doesn't check for duplicates (although with a normal usage, there shouldn't
+/// be any).
+public final function array< class > GetAllItemClasses() {
+ local array< class > result;
+ local ItemCard value;
+ local CollectionIterator iter;
+
+ for (iter = registeredCards.Iterate(); !iter.HasFinished(); iter.Next()) {
+ value = ItemCard(iter.Get());
+ if (value != none) {
+ result[result.length] = value.GetItemClass();
+ }
+ _.memory.Free(value);
+ }
+ iter.FreeSelf();
+ return result;
+}
+
+/// Returns array of names of all available items.
+public final function array GetItemsNames() {
+ local array emptyResult;
+
+ if (registeredCards != none) {
+ return registeredCards.GetTextKeys();
+ }
+ return emptyResult;
+}
+
+/// Called each time a new card is to be created and stored.
+///
+/// Must be reimplemented by child classes.
+protected function ItemCard MakeCard(class itemClass, BaseText itemName) {
+ return none;
+}
+
+/// Called each time a certain card is to be removed from storage.
+///
+/// Must be reimplemented by child classes
+/// (reimplementations SHOULD NOT DEALLOCATE `toDiscard`).
+protected function DiscardCard(ItemCard toDiscard) {
+}
+
+/// Find item card for the item that was stored with a specified
+/// case-insensitive name
+///
+/// Function only returns `none` if item with a given name wasn't found
+/// (or `none` was provided as an argument).
+protected final function ItemCard GetCard(BaseText itemName) {
+ local Text itemKey;
+ local ItemCard relevantCard;
+
+ if (itemName == none) return none;
+ if (registeredCards == none) return none;
+
+ /// The item name is transformed into lowercase, immutable value.
+ /// This facilitates the use of item names as keys in a [`HashTable`],
+ /// enabling case-insensitive matching.
+ itemKey = itemName.LowerCopy();
+ relevantCard = ItemCard(registeredCards.GetItem(itemKey));
+ _.memory.Free(itemKey);
+ return relevantCard;
+}
+
+defaultproperties {
+ errItemInvalidName = (l=LOG_Error,m="Attempt at registering item with class `%1` under an invalid name \"%2\" will be ignored.")
+ errItemDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Attempt at registering command `%3` with the same name will be ignored.")
+}
\ No newline at end of file
diff --git a/sources/BaseAPI/API/Commands/Tools/CommandsTool.uc b/sources/BaseAPI/API/Commands/Tools/CommandsTool.uc
new file mode 100644
index 0000000..5efd7c7
--- /dev/null
+++ b/sources/BaseAPI/API/Commands/Tools/CommandsTool.uc
@@ -0,0 +1,142 @@
+/**
+ * Author: dkanus
+ * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
+ * License: GPL
+ * Copyright 2023 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 CommandsTool extends CmdItemsTool;
+
+//! This is a base class for auxiliary objects that will be used for storing
+//! named [`Command`] instances.
+//!
+//! This storage class allows for efficient manipulation and retrieval of
+//! [`Command`]s, along with information about what use groups were authorized
+//! to use them.
+//!
+//! Additionally, this tool allows for efficient fetching of commands that
+//! belong to a particular *command group*.
+
+/// [`HashTable`] that maps a command group name to a set of command names that
+/// belong to it.
+var private HashTable groupedCommands;
+
+protected function Constructor() {
+ super.Constructor();
+ groupedCommands = _.collections.EmptyHashTable();
+}
+
+protected function Finalizer() {
+ super.Finalizer();
+ _.memory.Free(groupedCommands);
+ groupedCommands = none;
+}
+
+/// Returns all known command groups' names.
+public final function array GetGroupsNames() {
+ local array emptyResult;
+
+ if (groupedCommands != none) {
+ return groupedCommands.GetTextKeys();
+ }
+ return emptyResult;
+}
+
+/// Returns array of names of all available commands belonging to the specified
+/// group.
+public final function array GetCommandNamesInGroup(BaseText groupName) {
+ local int i;
+ local ArrayList commandNamesArray;
+ local array result;
+
+ if (groupedCommands == none) return result;
+ commandNamesArray = groupedCommands.GetArrayList(groupName);
+ if (commandNamesArray == none) return result;
+
+ for (i = 0; i < commandNamesArray.GetLength(); i += 1) {
+ result[result.length] = commandNamesArray.GetText(i);
+ }
+ _.memory.Free(commandNamesArray);
+ return result;
+}
+
+protected function ItemCard MakeCard(class commandClass, BaseText itemName) {
+ local Command newCommandInstance;
+ local ItemCard newCard;
+ local Text commandGroup;
+
+ if (class(commandClass) != none) {
+ newCommandInstance = Command(_.memory.Allocate(commandClass, true));
+ newCommandInstance.Initialize(itemName);
+ newCard = ItemCard(_.memory.Allocate(class'ItemCard'));
+ newCard.InitializeWithInstance(newCommandInstance);
+
+ // Guaranteed to be lower case (keys of [`HashTable`])
+ if (itemName != none) {
+ itemName = itemName.LowerCopy();
+ } else {
+ itemName = newCommandInstance.GetPreferredName();
+ }
+ commandGroup = newCommandInstance.GetGroupName();
+ AssociateGroupAndName(commandGroup, itemName);
+ _.memory.Free3(newCommandInstance, itemName, commandGroup);
+ }
+ return newCard;
+}
+
+protected function DiscardCard(ItemCard toDiscard) {
+ local Text groupKey, commandName;
+ local Command storedCommand;
+ local ArrayList listOfCommands;
+
+ if (toDiscard == none) return;
+ // Guaranteed to store a [`Command`]
+ storedCommand = Command(toDiscard.GetItem());
+ if (storedCommand == none) return;
+
+ // Guaranteed to be stored in a lower case
+ commandName = storedCommand.GetName();
+ listOfCommands = groupedCommands.GetArrayList(groupKey);
+ if (listOfCommands != none && commandName != none) {
+ listOfCommands.RemoveItem(commandName);
+ }
+ _.memory.Free2(commandName, storedCommand);
+}
+
+// Expect both arguments to be not `none`.
+// Expect both arguments to be lower-case.
+private final function AssociateGroupAndName(BaseText groupKey, BaseText commandName) {
+ local ArrayList listOfCommands;
+
+ if (groupedCommands != none) {
+ listOfCommands = groupedCommands.GetArrayList(groupKey);
+ if (listOfCommands == none) {
+ listOfCommands = _.collections.EmptyArrayList();
+ }
+ if (listOfCommands.Find(commandName) < 0) {
+ // `< 0` means not found
+ listOfCommands.AddItem(commandName);
+ }
+ // Set `listOfCommands` in case we've just created that array.
+ // Won't do anything if it is already recorded there.
+ groupedCommands.SetItem(groupKey, listOfCommands);
+ }
+}
+
+defaultproperties {
+ ruleBaseClass = class'Command';
+}
\ No newline at end of file
diff --git a/sources/BaseAPI/API/Commands/Tools/ItemCard.uc b/sources/BaseAPI/API/Commands/Tools/ItemCard.uc
new file mode 100644
index 0000000..e7cfea5
--- /dev/null
+++ b/sources/BaseAPI/API/Commands/Tools/ItemCard.uc
@@ -0,0 +1,177 @@
+/**
+ * Author: dkanus
+ * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
+ * License: GPL
+ * Copyright 2023 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 ItemCard extends AcediaObject;
+
+//! Utility class designed for storing either class of an object
+//! (possibly also a specific instance) along with authorization information:
+//! which user groups are allowed to use stored entity and with what level of
+//! permissions (defined by the name of a config with permissions).
+//!
+//! [`ItemCard`] has to be initialized with either [`InitializeWithClass()`] or
+//! [`InitializeWithInstance()`] before it can be used.
+
+/// Class of object that this card describes.
+var private class storedClass;
+/// Instance of an object (can also *optionally* be stored in this card)
+var private AcediaObject storedInstance;
+
+/// This [`HashTable`] maps authorized groups to their respective config names.
+///
+/// Each key represents an authorized group, and its corresponding value
+/// indicates the associated config name. If a key has a value of `none`,
+/// the default config (named "default") should be used for that group.
+var private HashTable groupToConfig;
+
+var LoggerAPI.Definition errGroupAlreadyHasConfig;
+
+protected function Finalizer() {
+ _.memory.Free2(storedInstance, groupToConfig);
+ storedInstance = none;
+ storedClass = none;
+ groupToConfig = none;
+}
+
+/// Initializes the caller [`ItemCard`] object with class to be stored.
+///
+/// Initialization can only be done once: once method returned `true`,
+/// all future calls will fail.
+///
+/// Returns `false` if caller was already initialized or `none` is provided as
+/// an argument. Otherwise succeeds and returns `true`.
+public function bool InitializeWithClass(class toStore) {
+ if (storedClass != none) return false;
+ if (toStore == none) return false;
+
+ storedClass = toStore;
+ groupToConfig = _.collections.EmptyHashTable();
+ return true;
+}
+
+/// Initializes the caller [`ItemCard`] object with an object to be stored.
+///
+/// Initialization can only be done once: once method returned `true`,
+/// all future calls will fail.
+///
+/// Returns `false` caller was already initialized or `none` is provided as
+/// an argument. Otherwise succeeds and returns `true`.
+public function bool InitializeWithInstance(AcediaObject toStore) {
+ if (storedClass != none) return false;
+ if (toStore == none) return false;
+
+ storedClass = toStore.class;
+ storedInstance = toStore;
+ storedInstance.NewRef();
+ groupToConfig = _.collections.EmptyHashTable();
+ return true;
+}
+
+/// Authorizes a new group to use the this card's item.
+///
+/// This function allows to specify the config name for a particular user group.
+/// If this config name is skipped (specified as `none`), then "default" will be
+/// used instead.
+///
+/// Function will return `true` if group was successfully authorized and
+/// `false` otherwise (either group already authorized or caller [`ItemCard`]
+/// isn't initialized).
+///
+/// # Errors
+///
+/// If specified group was already authorized to use card's item, then it
+/// will log an error message about it.
+public function bool AuthorizeGroupWithConfig(BaseText groupName, optional BaseText configName) {
+ local Text itemKey;
+ local Text storedConfigName;
+
+ if (storedClass == none) return false;
+ if (groupToConfig == none) return false;
+ if (groupName == none) return false;
+ if (groupName.IsEmpty()) return false;
+
+ /// Make group name immutable and have its characters have a uniform case to
+ /// be usable as case-insensitive keys for [`HashTable`].
+ itemKey = groupName.LowerCopy();
+ storedConfigName = groupToConfig.GetText(itemKey);
+ if (storedConfigName != none) {
+ _.logger.Auto(errGroupAlreadyHasConfig)
+ .ArgClass(storedClass)
+ .Arg(groupName.Copy())
+ .Arg(storedConfigName)
+ .Arg(configName.Copy());
+ _.memory.Free(itemKey);
+ return false;
+ }
+ // We don't actually record "default" value at this point, instead opting
+ // to return "default" in getter functions in case stored `configName`
+ // is `none`.
+ groupToConfig.SetItem(itemKey, configName);
+ _.memory.Free(itemKey);
+ return true;
+}
+
+/// Returns item instance for the caller [`ItemCard`].
+///
+/// Returns `none` iff this card wasn't initialized with an instance.
+public function AcediaObject GetItem() {
+ if (storedInstance != none) {
+ storedInstance.NewRef();
+ }
+ return storedInstance;
+}
+
+/// Returns item class for the caller [`ItemCard`].
+///
+/// Returns `none` iff this card wasn't initialized.
+public function class GetItemClass() {
+ return storedClass;
+}
+
+/// Returns the name of config that was authorized for the specified group.
+///
+/// Returns `none` if group wasn't authorized, otherwise guaranteed to
+/// return non-`none` and non-empty `Text` value.
+public function Text GetConfigNameForGroup(BaseText groupName) {
+ local Text groupNameAsKey, result;
+
+ if (storedClass == none) return none;
+ if (groupToConfig == none) return none;
+ if (groupName == none) return none;
+
+ /// Make group name immutable and have its characters a uniform case to
+ /// be usable as case-insensitive keys for [`HashTable`]
+ groupNameAsKey = groupName.LowerCopy();
+ if (groupToConfig.HasKey(groupNameAsKey)) {
+ result = groupToConfig.GetText(groupNameAsKey);
+ if (result == none) {
+ // If we do have specified group recorded as a key, then we must
+ // return non-`none` config name, defaulting to "default" value
+ // if none was provided
+ result = P("default").Copy();
+ }
+ }
+ _.memory.Free(groupNameAsKey);
+ return result;
+}
+
+defaultproperties {
+ errGroupAlreadyHasConfig = (l=LOG_Error,m="Item `%1` is already added to group '%2' with config '%3'. Attempt to add it with config '%4' is ignored.")
+}
\ No newline at end of file
diff --git a/sources/BaseAPI/API/Commands/Tools/VotingsTool.uc b/sources/BaseAPI/API/Commands/Tools/VotingsTool.uc
new file mode 100644
index 0000000..c0a6b1e
--- /dev/null
+++ b/sources/BaseAPI/API/Commands/Tools/VotingsTool.uc
@@ -0,0 +1,119 @@
+/**
+ * Author: dkanus
+ * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
+ * License: GPL
+ * Copyright 2023 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 VotingsTool extends CmdItemsTool
+ dependson(CommandAPI);
+
+//! This is a base class for auxiliary objects that will be used for storing
+//! named [`Voting`] classes.
+//!
+//! This storage class allows for efficient manipulation and retrieval of
+//! [`Voting`] classes, along with information about what use groups were
+//! authorized to use them.
+//!
+//! Additionally this tool is used to keep track of the currently ongoing
+//! voting, preventing [`CommandsAPI`] from starting several votings at once.
+
+/// Currently running voting process.
+/// This tool doesn't actively track when voting ends, so reference can be
+/// non-`none` even if voting has already ended. Instead `DropFinishedVoting()`
+/// method is used as needed to figure out whether that voting has ended and
+/// should be deallocated.
+var private Voting currentVoting;
+
+protected function Finalizer() {
+ super.Finalizer();
+ _.memory.Free(currentVoting);
+ currentVoting = none;
+}
+
+/// Starts a voting process with a given name, returning its result.
+public final function CommandAPI.StartVotingResult StartVoting(
+ CommandAPI.VotingConfigInfo votingData,
+ HashTable arguments
+) {
+ local CommandAPI.StartVotingResult result;
+ DropFinishedVoting();
+ if (currentVoting != none) {
+ return SVR_AlreadyInProgress;
+ }
+ if (votingData.votingClass == none) {
+ return SVR_UnknownVoting;
+ }
+ currentVoting = Voting(_.memory.Allocate(votingData.votingClass));
+ result = currentVoting.Start(votingData.config, arguments);
+ if (result != SVR_Success) {
+ _.memory.Free(currentVoting);
+ currentVoting = none;
+ }
+ return result;
+}
+
+/// Returns `true` iff some voting is currently active.
+public final function bool IsVotingRunning() {
+ DropFinishedVoting();
+ return (currentVoting != none);
+}
+
+/// Returns instance of the active voting.
+///
+/// `none` iff no voting is currently active.
+public final function Voting GetCurrentVoting() {
+ DropFinishedVoting();
+ if (currentVoting != none) {
+ currentVoting.NewRef();
+ }
+ return currentVoting;
+}
+
+protected function ItemCard MakeCard(class votingClass, BaseText itemName) {
+ local ItemCard newCard;
+
+ if (class(votingClass) != none) {
+ newCard = ItemCard(_.memory.Allocate(class'ItemCard'));
+ newCard.InitializeWithClass(votingClass);
+ }
+ return newCard;
+}
+
+private final function class GetVoting(BaseText itemName) {
+ local ItemCard relevantCard;
+ local class result;
+
+ relevantCard = GetCard(itemName);
+ if (relevantCard != none) {
+ result = class(relevantCard.GetItemClass());
+ }
+ _.memory.Free(relevantCard);
+ return result;
+}
+
+// Clears `currentVoting` if it has already finished
+private final function DropFinishedVoting() {
+ if (currentVoting != none && currentVoting.HasEnded()) {
+ _.memory.Free(currentVoting);
+ currentVoting = none;
+ }
+}
+
+defaultproperties {
+ ruleBaseClass = class'Voting'
+}
\ No newline at end of file