From a7f1a985486cc824830ad60dca6167ad2d002dc7 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sun, 20 Aug 2023 18:15:09 +0700 Subject: [PATCH] Change CommandAPI and feature to use tools --- sources/BaseAPI/API/Commands/CommandAPI.uc | 1568 +++++++++++++++-- .../API/Commands/CommandRegistrationJob.uc | 59 +- sources/BaseAPI/API/Commands/Commands.uc | 182 +- .../BaseAPI/API/Commands/Commands_Feature.uc | 818 +++++---- 4 files changed, 2081 insertions(+), 546 deletions(-) diff --git a/sources/BaseAPI/API/Commands/CommandAPI.uc b/sources/BaseAPI/API/Commands/CommandAPI.uc index 52c6112..c6b47ae 100644 --- a/sources/BaseAPI/API/Commands/CommandAPI.uc +++ b/sources/BaseAPI/API/Commands/CommandAPI.uc @@ -21,82 +21,550 @@ */ class CommandAPI extends AcediaObject; -// Classes registered to be registered in async way -var private array< class > pendingClasses; -// Job that is supposed to register pending commands +/// Possible statuses of command locks. +/// +/// Command locks can prevent certain commands from being used. +/// A good example is having a "cheat" lock that commands can check to before +/// doing anything that can be considered cheating. +/// Their purpose is to track the "purity" of the game matches +/// (e.g. whether cheats were used). +enum LockStatus { + /// Lock is currently closed, preventing commands from doing actions that + /// might require it opened. + /// Lock can still be opened. + LS_Closed, + /// Lock is currently opened, allowing commands to perform actions that + /// might require it opened. + /// Lock can still be closed. + LS_Open, + /// Lock is currently closed, preventing commands from doing actions that + /// might require it opened. + /// Lock can no longer be opened during the game session. + LS_ClosedFixed, + /// Lock is currently opened, allowing commands to perform actions that + /// might require it opened. + /// Lock can no longer be closed during the game session. + /// For "cheat" lock, for example, it can happen if someone has already + /// used cheats. + LS_OpenFixed +}; + +/// Describes possible outcomes of starting a voting by its name +enum StartVotingResult { + /// Voting was successfully started. + SVR_Success, + /// There is no people that are allowed to vote. + SVR_NoVoters, + /// Voting itself decided to reject being started, most likely because of + /// the specified arguments. + SVR_Rejected, + /// Voting wasn't started because another one was still in progress. + SVR_AlreadyInProgress, + /// Voting wasn't started because voting with that name hasn't been + /// registered. + SVR_UnknownVoting, + /// `CommandAPI` isn't functioning properly. + SVR_InvalidState +}; + +/// Struct for storing the result of "resolving" [`Command`] access for +/// some user. +struct CommandConfigInfo { + /// Instance of the command. If it equals to `none`, then [`configName`] + /// will also equal `none`. + var public Command instance; + /// Config that determines permissions for the command [`instance`]. + /// Can equal to `none` if: + /// + /// * [`Command`] class didn't setup a custom permissions config. + /// * Using provided instance is forbidden + /// (in this case `usageForbidden` will be set to `true`). + var public CommandPermissions config; + /// Set to `true` in case using provided command if forbidden. + var public bool usageForbidden; +}; + +/// Struct for storing the result of "resolving" [`Voting`] access for +/// some user. +struct VotingConfigInfo { + /// [`Voting`]'s class. If it equals to `none`, then [`configName`] + /// will also equal `none`. + var public class votingClass; + /// Config that determines permissions for the [`votingClass`]. + /// Can equal to `none` if using provided instance is forbidden + /// (in this case `usageForbidden` will be set to `true`). + var public VotingPermissions config; + /// Set to `true` in case using provided voting if forbidden. + var public bool usageForbidden; +}; + +/// Internal enum that describes all types of scheduled jobs that this API can +/// produce. +enum AsyncJobType { + CAJT_AddCommand, + CAJT_AuthorizeCommand, + CAJT_AddVoting, + CAJT_AuthorizeVoting +}; + +/// Internal struct that can describe data for any scheduled job that this API +/// can produce. +struct AsyncTask { + var public AsyncJobType type; + // For all jobs + var public Text entityName; + // For `CAJT_Add...` + var public class entityClass; + // For `CAJT_Authorize` + var public Text userGroup; + // For `CAJT_Authorize...` + var public Text configName; +}; + +/// Internal struct for storing all the utility objects that `Commands_Feature` +/// uses. +struct CommandFeatureTools { + var public CommandsTool commands; + var public VotingsTool votings; +}; + +/// Internal struct for representing the result of the resolve command by either +/// [`CommandsTool`] or [`VotingsTool`]. +/// +/// Defined in `CommandAPI` instead of their base [`CmdItemsTool`] to avoid +/// compiler issues with resolving `dependson`-graph. +struct ItemConfigInfo { + var public AcediaObject instance; + var public class class; + // Name of config that determines permissions for the item [`instance`] + var public Text configName; +}; + +/// Classes registered to be added in async way +var private array pendingAsyncJobs; +/// Job that is supposed to register pending commands, will be asking for new +/// jobs from [`pendingAsyncJobs`] once it completes its current task. var private CommandRegistrationJob registeringJob; -// Saves `HashTable` with command locks. -// Locks are simply boolean switches that mark for commands whether they can be executed. -// -// Lock is considered "unlocked" if this `HashTable` stores `true` at the key with its name -// and `false` otherwise. +/// Saves `HashTable` with command locks. +/// Locks are simply boolean switches that mark for commands whether they +/// can be executed. +/// +/// Lock is considered "unlocked" if this `HashTable` stores `true` at the key +/// with its name and `false` otherwise. var private HashTable commandLocks; -var private Commands_Feature commandsFeature; +var private CommandsTool commandsTool; +var private VotingsTool votingsTool; +var private int commandsToolLifeVersion; +var private int votingsToolLifeVersion; -// DO NOT CALL MANUALLY -public final /*internal*/ function class _popPending() { - local class result; +var protected CommandsAPI_OnCommandAdded_Signal onCommandAddedSignal; +var protected CommandsAPI_OnCommandRemoved_Signal onCommandRemovedSignal; +var protected CommandsAPI_OnVotingAdded_Signal onVotingAddedSignal; +var protected CommandsAPI_OnVotingRemoved_Signal onVotingRemovedSignal; + +protected function Constructor() { + onCommandAddedSignal = CommandsAPI_OnCommandAdded_Signal( + _.memory.Allocate(class'CommandsAPI_OnCommandAdded_Signal')); + onCommandRemovedSignal = CommandsAPI_OnCommandRemoved_Signal( + _.memory.Allocate(class'CommandsAPI_OnCommandRemoved_Signal')); + onVotingAddedSignal = CommandsAPI_OnVotingAdded_Signal( + _.memory.Allocate(class'CommandsAPI_OnVotingAdded_Signal')); + onVotingRemovedSignal = CommandsAPI_OnVotingRemoved_Signal( + _.memory.Allocate(class'CommandsAPI_OnVotingRemoved_Signal')); +} + +protected function Finalizer() { + _.memory.Free(onCommandAddedSignal); + _.memory.Free(onCommandRemovedSignal); + _.memory.Free(onVotingAddedSignal); + _.memory.Free(onVotingRemovedSignal); + onCommandAddedSignal = none; + onCommandRemovedSignal = none; + onVotingAddedSignal = none; + onVotingRemovedSignal = none; +} + +/// Signal that will be emitted when a new [`Command`] class is successfully +/// added through this API. +/// +/// # Slot description +/// +/// bool (class addedClass, Text usedName) +/// +/// ## Parameters +/// +/// * [`addedClass`]: Class of the command that got added. +/// * [`usedName`]: Name, under which command class was added. +public /*signal*/ function CommandsAPI_OnCommandAdded_Slot OnCommandAdded(AcediaObject receiver) { + return CommandsAPI_OnCommandAdded_Slot(onCommandAddedSignal.NewSlot(receiver)); +} + +/// Signal that will be emitted when a [`Command`] class is removed through +/// this API. +/// +/// # Slot description +/// +/// bool (class removedClass) +/// +/// ## Parameters +/// +/// * [`removedClass`]: Class of the command that got removed. +public /*signal*/ function CommandsAPI_OnCommandRemoved_Slot OnCommandRemoved( + AcediaObject receiver +) { + return CommandsAPI_OnCommandRemoved_Slot(onCommandRemovedSignal.NewSlot(receiver)); +} + +/// Signal that will be emitted when a new [`Voting`] class is successfully +/// added through this API. +/// +/// # Slot description +/// +/// bool (class addedClass, Text usedName) +/// +/// ## Parameters +/// +/// * [`addedClass`]: Class of the voting that got added. +/// * [`usedName`]: Name, under which voting class was added. +public /*signal*/ function CommandsAPI_OnVotingAdded_Slot OnVotingAdded(AcediaObject receiver) { + return CommandsAPI_OnVotingAdded_Slot(onVotingAddedSignal.NewSlot(receiver)); +} + +/// Signal that will be emitted when a [`Voting`] class is removed through +/// this API. +/// +/// # Slot description +/// +/// bool (class removedClass) +/// +/// ## Parameters +/// +/// * [`removedClass`]: Class of the voting that got removed. +public /*signal*/ function CommandsAPI_OnVotingRemoved_Slot OnVotingRemoved(AcediaObject receiver) { + return CommandsAPI_OnVotingRemoved_Slot(onVotingRemovedSignal.NewSlot(receiver)); +} + +/// Checks if `Commands_Feature` is enabled, which is required for this API +/// to be functional. +public final function bool AreCommandsEnabled() { + local Commands_Feature feature; + + feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); + _.memory.Free(feature); + // We can still compare to `none` even after deallocation to see if + // `GetEnabledInstance()` returned a valid instance. + return (feature != none); +} + +/// Returns array of names of all available commands. +/// +/// Resulting array cannot contain duplicates. +public final function array GetAllCommandNames() { + local array emptyResult; + + if (VerifyCommandsTool()) { + return commandsTool.GetItemsNames(); + } + return emptyResult; +} + +/// Registers given command class, making it available via [`Execute()`]. +/// +/// Optionally a [`BaseText`] can be specified to be used as this command's name +/// (the main name, that command aliases are to be resolved into). +/// If name parameter is omitted (specified as `none`) or constitutes an empty +/// [`BaseText`], a command's preferred name will be used. +/// +/// If command name is specified as `none`, then "default" will be +/// used instead. For non-`none` values, invalid name (according to +/// [`BaseText::IsValidName()`] method) will also prevent the group from being +/// authorized. +/// +/// # Errors +/// +/// Returns `true` if command was successfully registered and `false` +/// otherwise`. +/// +/// If [`commandClass`] provides command with a name that is already taken +/// (comparison is case-insensitive) by a different command - a warning will be +/// logged and command class won't be registered. +/// +/// If `commandClass` provides command with an empty name (and it isn't +/// overridden by the argument) - a warning will be logged and command class +/// won't be registered. +public final function bool AddCommand( + class commandClass, + optional BaseText commandName +) { + local bool result; + local Text immutableCommandName; - if (pendingClasses.length == 0) { - return none; + if (commandClass == none) { + return false; + } + if (VerifyCommandsTool()) { + if (commandName != none) { + immutableCommandName = commandName.Copy(); + } else { + immutableCommandName = commandClass.static.GetPreferredName(); + } + result = commandsTool.AddItemClass(commandClass, immutableCommandName); + if (result) { + onCommandAddedSignal.Emit(commandClass, immutableCommandName); + } + _.memory.Free(immutableCommandName); } - result = pendingClasses[0]; - pendingClasses.Remove(0, 1); + return false; +} + +/// Registers given command class, making it available via [`Execute()`]. +/// +/// Optionally a [`BaseText`] can be specified to be used as this command's name +/// (the main name, that command aliases are to be resolved into). +/// If name parameter is omitted (specified as empty [`string`]), a command's +/// default name (defined via its [`CommandDataBuilder`]) will be used. +/// +/// Invalid name (according to [`BaseText::IsValidName()`] method) will prevent +/// the [`Command`] from being authorized. +/// +/// # Errors +/// +/// Returns `true` if command was successfully registered and +/// `false` otherwise`. +/// +/// If `commandClass` provides command with a name that is already taken +/// (comparison is case-insensitive) by a different command - a warning will be +/// logged and command class won't be registered. +/// +/// If `commandClass` provides command with an empty name (and it isn't +/// overridden by the argument) - a warning will be logged and command class +/// won't be registered. +public final function bool AddCommand_S( + class commandClass, + optional string commandName +) { + local bool result; + local MutableText wrapper; + + if (commandName != "") { + wrapper = _.text.FromStringM(commandName); + } + result = AddCommand(commandClass, wrapper); + _.memory.Free(wrapper); return result; } -// DO NOT CALL MANUALLY -public final /*internal*/ function _reloadFeature() { - if (commandsFeature != none) { - commandsFeature.FreeSelf(); - commandsFeature = none; +/// Registers given command class asynchronously, making it available +/// via [`Execute()`]. +/// +/// Doesn't register commands immediately, instead scheduling it to be done at +/// a later moment in time. This can help to reduce amount of work we do every +/// tick during server startup, therefore avoiding crashed due to the faulty +/// infinite loop detection. Different async calls from [`CommandAPI`] are +/// guaranteed to be handled in the order they were called. +/// +/// Optionally a [`BaseText`] can be specified to be used as this command's name +/// (the main name, that command aliases are to be resolved into). +/// If name parameter is omitted (specified as `none`) or constitutes an empty +/// [`BaseText`], a command's default name (defined via its +/// [`CommandDataBuilder`]) will be used. +/// +/// If command name is specified as `none`, then "default" will be +/// used instead. For non-`none` values, invalid name (according to +/// [`BaseText::IsValidName()`] method) will also prevent the group from being +/// authorized. +/// +/// # Errors +/// +/// Returns `true` if command was successfully registered and +/// `false` otherwise`. +/// +/// If [`commandClass`] provides command with a name that is already taken +/// (comparison is case-insensitive) by a different command - a warning will be +/// logged and command class won't be registered. +/// +/// If [`commandClass`] provides command with an empty name (and it isn't +/// overridden by the argument) - a warning will be logged and command class +/// won't be registered. +/// +/// Warnings might be logged *after* the call itself, when the command class is +/// actually added. +public final function AddCommandAsync( + class commandClass, + optional BaseText commandName +) { + local AsyncTask newJob; + + if (!VerifyCommandsTool()) { + return; + } + newJob.type = CAJT_AddCommand; + newJob.entityClass = commandClass; + if (commandName != none) { + newJob.entityName = commandName.Copy(); + } + pendingAsyncJobs[pendingAsyncJobs.length] = newJob; + if (registeringJob == none || registeringJob.IsCompleted()) { + _.memory.Free(registeringJob); + registeringJob = CommandRegistrationJob(_.memory.Allocate(class'CommandRegistrationJob')); + _.scheduler.AddJob(registeringJob); } - commandsFeature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); } -/// Checks if `Commands_Feature` is enabled, which is required for this API to be functional. -public final function bool AreCommandsEnabled() { - // `Commands_Feature` is responsible for updating us with an actually enabled instance - return (commandsFeature != none); +/// Registers given command class asynchronously, making it available +/// via [`Execute()`]. +/// +/// Doesn't register commands immediately, instead scheduling it to be done at +/// a later moment in time. This can help to reduce amount of work we do every +/// tick during server startup, therefore avoiding crashed due to the faulty +/// infinite loop detection. Different async calls from [`CommandAPI`] are +/// guaranteed to be handled in the order they were called. +/// +/// Optionally a [`BaseText`] can be specified to be used as this command's name +/// (the main name, that command aliases are to be resolved into). +/// If name parameter is omitted (specified as empty [`string`]), a command's +/// default name (defined via its [`CommandDataBuilder`]) will be used. +/// +/// Invalid name (according to [`BaseText::IsValidName()`] method) will prevent +/// the [`Command`] from being authorized. +/// +/// # Errors +/// +/// Returns `true` if command was successfully registered and +/// `false` otherwise`. +/// +/// If [`commandClass`] provides command with a name that is already taken +/// (comparison is case-insensitive) by a different command - a warning will be +/// logged and command class won't be registered. +/// +/// If [`commandClass`] provides command with an empty name (and it isn't +/// overridden by the argument) - a warning will be logged and command class +/// won't be registered. +/// +/// Warnings might be logged *after* the call itself, when the command class is +/// actually added. +public final function AddCommandAsync_S( + class commandClass, + optional string commandName +) { + local MutableText wrapper; + + if (commandName != "") { + wrapper = _.text.FromStringM(commandName); + } + AddCommandAsync(commandClass, wrapper); + _.memory.Free(wrapper); } -/// Registers given command class, making it available via `Execute()`. +/// Authorizes new user group to use the specified command, optionally +/// specifying name of the config (config's class is determined by the +/// [`Command`]'s class) that describes permissions of that group. /// -/// Unless you need command right now, it is recommended to use `RegisterCommandAsync()` instead. +/// Method must be called after [`Command`] with a given name is added. /// -/// Returns `true` if command was successfully registered and `false` otherwise`. +/// If config name is specified as `none`, then "default" will be +/// used instead. For non-`none` values, invalid name (according to +/// [`BaseText::IsValidName()`] method) will also prevent the group from being +/// authorized. +/// +/// Function will return `true` if group was successfully authorized and +/// `false` otherwise (either group already authorized or no command with +/// specified name was added in the caller tool). /// /// # Errors /// -/// If `commandClass` provides command with a name that is already taken (comparison is -/// case-insensitive) by a different command - a warning will be logged and newly passed -/// `commandClass` discarded. -public final function bool RegisterCommand(class commandClass) { - if (commandsFeature != none) { - return commandsFeature.RegisterCommand(commandClass); +/// If specified group was already authorized to use a command, then it +/// will log a warning message about it. +public function bool AuthorizeCommandUsage( + BaseText commandName, + BaseText groupName, + optional BaseText configName +) { + if (VerifyCommandsTool()) { + return commandsTool.AuthorizeUsage(commandName, groupName, configName); } return false; } -/// Registers given command class asynchronously, making it available via `Execute()`. +/// Authorizes new user group to use the specified command, optionally +/// specifying name of the config (config's class is determined by the +/// [`Command`]'s class) that describes permissions of that group. +/// +/// Method must be called after [`Command`] with a given name is added. +/// +/// If this config name is specified as empty, then "default" will be +/// used instead. For non-empty values, an invalid name (according to +/// [`BaseText::IsValidName()`] method) will prevent the group from being +/// authorized. +/// +/// # Errors +/// +/// If specified group was already authorized to use a command, then it +/// will log a warning message about it. +public function bool AuthorizeCommandUsage_S( + string commandName, + string userGroupName, + optional string configName +) { + local bool result; + local MutableText wrapperVotingName, wrapperGroupName, wrapperConfigName; + + wrapperVotingName = _.text.FromStringM(commandName); + wrapperGroupName = _.text.FromStringM(userGroupName); + wrapperConfigName = _.text.FromStringM(configName); + result = AuthorizeCommandUsage(wrapperVotingName, wrapperGroupName, wrapperConfigName); + _.memory.Free3(wrapperVotingName, wrapperGroupName, wrapperConfigName); + return result; +} + +/// Authorizes new user group to use the specified command, optionally +/// specifying name of the config (config's class is determined by the +/// [`Command`]'s class) that describes permissions of that group. +/// +/// Method must be called after [`Command`] with a given name is added. +/// +/// Doesn't authorize commands immediately, instead scheduling it to be done at +/// a later moment in time. This can help to reduce amount of work we do every +/// tick during server startup, therefore avoiding crashed due to the faulty +/// infinite loop detection. Different async calls from [`CommandAPI`] are +/// guaranteed to be handled in the order they were called. +/// +/// If config name is specified as `none`, then "default" will be +/// used instead. For non-`none` values, invalid name (according to +/// [`BaseText::IsValidName()`] method) will also prevent the group from being +/// authorized. /// -/// Doesn't register commands immediately, instead scheduling it to be done at a later moment in -/// time, allowing. -/// This can help to reduce amount of work we do every tick during server startup, therefore -/// avoiding crashed due to the faulty infinite loop detection. +/// Function will return `true` if group was successfully authorized and +/// `false` otherwise (either group already authorized or no command with +/// specified name was added in the caller tool). /// /// # Errors /// -/// If `commandClass` provides command with a name that is already taken (comparison is -/// case-insensitive) by a different command - a warning will be logged and newly passed -/// `commandClass` discarded. -public final function RegisterCommandAsync(class commandClass) { - if (commandsFeature == none) { +/// If specified group was already authorized to use a command, then it +/// will log a warning message about it. +public function AuthorizeCommandUsageAsync( + BaseText commandName, + BaseText userGroupName, + optional BaseText configName +) { + local AsyncTask newTask; + + if (!VerifyCommandsTool()) { return; } - pendingClasses[pendingClasses.length] = commandClass; + newTask.type = CAJT_AuthorizeCommand; + if (commandName != none) { + newTask.entityName = commandName.Copy(); + } + if (userGroupName != none) { + newTask.userGroup = userGroupName.Copy(); + } + if (configName != none) { + newTask.configName = configName.Copy(); + } + pendingAsyncJobs[pendingAsyncJobs.length] = newTask; if (registeringJob == none || registeringJob.IsCompleted()) { _.memory.Free(registeringJob); registeringJob = CommandRegistrationJob(_.memory.Allocate(class'CommandRegistrationJob')); @@ -104,124 +572,1006 @@ public final function RegisterCommandAsync(class commandClass) { } } +/// Authorizes new user group to use the specified command, optionally +/// specifying name of the config (config's class is determined by the +/// [`Command`]'s class) that describes permissions of that group. +/// +/// Method must be called after [`Command`] with a given name is added. +/// +/// Doesn't authorize commands immediately, instead scheduling it to be done at +/// a later moment in time. This can help to reduce amount of work we do every +/// tick during server startup, therefore avoiding crashed due to the faulty +/// infinite loop detection. Different async calls from [`CommandAPI`] are +/// guaranteed to be handled in the order they were called. +/// +/// If this config name is specified as empty, then "default" will be +/// used instead. For non-empty values, an invalid name (according to +/// [`BaseText::IsValidName()`] method) will prevent the group from being +/// authorized. +/// +/// Function will return `true` if group was successfully authorized and +/// `false` otherwise (either group already authorized or no command with +/// specified name was added in the caller tool). +/// +/// # Errors +/// +/// If specified group was already authorized to use a command, then it +/// will log a warning message about it. +public function AuthorizeCommandUsageAsync_S( + string commandName, + string userGroupName, + optional string configName +) { + local MutableText wrapperCommandName, wrapperGroupName, wrapperConfigName; + + wrapperCommandName = _.text.FromStringM(commandName); + wrapperGroupName = _.text.FromStringM(userGroupName); + wrapperConfigName = _.text.FromStringM(configName); + AuthorizeCommandUsageAsync(wrapperCommandName, wrapperGroupName, wrapperConfigName); + _.memory.Free3(wrapperCommandName, wrapperGroupName, wrapperConfigName); +} + /// Removes command of given class from the list of registered commands. /// -/// Removing once registered commands is not an action that is expected to be performed under normal -/// circumstances and it is not efficient. -/// It is linear on the current amount of commands. -public final function RemoveCommand(class commandClass) { - if (commandsFeature != none) { - commandsFeature.RemoveCommand(commandClass); +/// Removing once registered commands 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 commands). +/// +/// Returns `true` if successfully removed registered [`Command`] class and +/// `false` otherwise (command wasn't registered). +public final function bool RemoveCommand(class commandClass) { + local bool result; + + if (VerifyCommandsTool()) { + result = commandsTool.RemoveItemClass(commandClass); + if (result) { + onCommandRemovedSignal.Emit(commandClass); + } } + return result; } -/// Executes command based on the input. +/// Returns instance of the [`Command`] that was added under a specified name. +public final function Command GetCommand(BaseText commandName) { + local ItemConfigInfo intermediaryResult; + + if (VerifyCommandsTool()) { + // `none` means we'll get instance + class, but without config, + // so nothing to deallocate in `intermediaryResult` + intermediaryResult = commandsTool.ResolveItem(commandName, none); + return Command(intermediaryResult.instance); + } + return none; +} + +/// Returns instance of the [`Command`] that was added under a specified name. +public final function Command GetCommand_S(string commandName) { + local Command result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(commandName); + result = GetCommand(wrapper); + _.memory.Free(wrapper); + return result; +} + +/// Returns pair of [`Command`] and config name based on a given +/// case-insensitive name and text ID of the caller player. /// -/// Takes [`commandLine`] as input with command's call, finds appropriate registered command -/// instance and executes it with parameters specified in the [`commandLine`]. +/// Function only returns `none` for [`Command`] instance if [`Command`] with +/// a given name wasn't found. +public final function CommandConfigInfo ResolveCommandForTextID(BaseText itemName, BaseText id) { + local ItemConfigInfo intermediaryResult; + local CommandConfigInfo result; + + if (VerifyCommandsTool()) { + intermediaryResult = commandsTool.ResolveItem(itemName, id); + result.instance = Command(intermediaryResult.instance); + if (result.instance == none) { + return result; + } + if (intermediaryResult.configName == none) { + result.usageForbidden = true; + } else { + result.config = result.instance.LoadConfig(intermediaryResult.configName); + _.memory.Free(intermediaryResult.configName); + } + } + return result; +} + +/// Returns pair of [`Command`] and config name based on a given +/// case-insensitive name and text ID of the caller player. /// -/// [`callerPlayer`] has to be specified and represents instigator of this command that will receive -/// appropriate result/error messages. +/// Function only returns `none` for [`Command`] instance if [`Command`] with +/// a given name wasn't found. +public final function CommandConfigInfo ResolveCommandForTextID_S(string itemName, string id) { + local CommandConfigInfo result; + local MutableText wrapperItemname, wrapperID; + + wrapperItemname = _.text.FromStringM(itemName); + wrapperID = _.text.FromStringM(id); + result = ResolveCommandForTextID(wrapperItemname, wrapperID); + _.memory.Free2(wrapperItemname, wrapperID); + return result; +} + +/// Returns pair of [`Command`] and config name based on a given +/// case-insensitive name and [`UserID`] of the caller player. /// -/// Returns `true` iff command was successfully executed. +/// Function only returns `none` for [`Command`] instance if [`Command`] with +/// a given name wasn't found or provided id was `none`. +public final function CommandConfigInfo ResolveCommandForUserID(BaseText itemName, UserID id) { + local CommandConfigInfo result; + local Text textID; + + if (itemName == none) return result; + if (id == none) return result; + textID = id.GetUniqueID(); + if (textID == none) return result; + + result = ResolveCommandForTextID(itemName, textID); + textID.FreeSelf(); + return result; +} + +/// Returns pair of [`Command`] and config name based on a given +/// case-insensitive name and [`UserID`] of the caller player. /// -/// # Errors +/// Function only returns `none` for [`Command`] instance if [`Command`] with +/// a given name wasn't found or provided id was `none`. +public final function CommandConfigInfo ResolveCommandForUserID_S(string itemName, UserID id) { + local CommandConfigInfo result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(itemName); + result = ResolveCommandForUserID(wrapper, id); + _.memory.Free(wrapper); + return result; +} + +/// Returns pair of [`Command`] and config name based on a given +/// case-insensitive name and [`User`] of the caller player. /// -/// Doesn't log any errors, but can complain about errors in name or parameters to -/// the [`callerPlayer`] -public final function Execute(BaseText commandLine, EPlayer callerPlayer) { - if (commandsFeature != none) { - commandsFeature.HandleInput(commandLine, callerPlayer); +/// Function only returns `none` for [`Command`] instance if [`Command`] with +/// a given name wasn't found or provided id was `none`. +public final function CommandConfigInfo ResolveCommandForUser(BaseText itemName, User user) { + local CommandConfigInfo result; + local UserID id; + + if (itemName == none) return result; + if (user == none) return result; + + id = user.GetID(); + result = ResolveCommandForUserID(itemName, id); + _.memory.Free(id); + return result; +} + +/// Returns pair of [`Command`] and config name based on a given +/// case-insensitive name and [`User`] of the caller player. +/// +/// Function only returns `none` for [`Command`] instance if [`Command`] with +/// a given name wasn't found or provided id was `none`. +public final function CommandConfigInfo ResolveCommandForUser_S(string itemName, User user) { + local CommandConfigInfo result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(itemName); + result = ResolveCommandForUser(wrapper, user); + _.memory.Free(wrapper); + return result; +} + +/// Returns all available command groups' names. +public final function array GetGroupsNames() { + local array emptyResult; + + if (VerifyCommandsTool()) { + return commandsTool.GetGroupsNames(); } + return emptyResult; } -/// Executes command based on the input. +/// Returns all available command groups' names. +public final function array GetGroupsNames_S() { + local array emptyResult; + + if (VerifyCommandsTool()) { + return _.text.IntoStrings(commandsTool.GetGroupsNames()); + } + return emptyResult; +} + +/// Returns array of names of all available commands belonging to the specified +/// command group. +public final function array GetCommandNamesInGroup(BaseText groupName) { + local array emptyResult; + + if (VerifyCommandsTool()) { + return commandsTool.GetCommandNamesInGroup(groupName); + } + return emptyResult; +} + +/// Returns array of names of all available commands belonging to the specifie +/// command group. +public final function array GetCommandNamesInGroup_S(string groupName) { + local array result; + local MutableText wrapper; + + if (VerifyCommandsTool()) { + wrapper = _.text.FromStringM(groupName); + result = _.text.IntoStrings(commandsTool.GetCommandNamesInGroup(wrapper)); + _.memory.Free(wrapper); + } + return result; +} + +/// Executes command based on the textual input with a given instigator. /// -/// Takes [`commandLine`] as input with command's call, finds appropriate registered command -/// instance and executes it with parameters specified in the [`commandLine`]. +/// Input should be provided in a form that players are expected to use. +/// For example, "mutate inventory @all" or "say !inventory @all" will both +/// translate into calling this method with "inventory @all" argument. /// -/// [`callerPlayer`] has to be specified and represents instigator of this command that will receive -/// appropriate result/error messages. +/// Command's instigator will receive appropriate result/error messages. /// -/// Returns `true` iff command was successfully executed. +/// # Errors +/// +/// Doesn't log any errors, but can complain about errors in name or parameters +/// to the [`instigator`]. +public final function Execute(BaseText commandLine, EPlayer instigator) { + local Commands_Feature feature; + + feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); + if (feature != none) { + feature.HandleInput(commandLine, instigator); + } + _.memory.Free(feature); +} + +/// Executes command based on the textual input with a given instigator. +/// +/// Input should be provided in a form that players are expected to use. +/// For example, "mutate inventory @all" or "say !inventory @all" will both +/// translate into calling this method with "inventory @all" argument. +/// +/// Command's instigator will receive appropriate result/error messages. /// /// # Errors /// -/// Doesn't log any errors, but can complain about errors in name or parameters to -/// the [`callerPlayer`] -public final function Execute_S(string commandLine, EPlayer callerPlayer) { +/// Doesn't log any errors, but can complain about errors in name or parameters +/// to the [`instigator`]. +public final function Execute_S(string commandLine, EPlayer instigator) { local MutableText wrapper; + local Commands_Feature feature; - if (commandsFeature != none) { + feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); + if (feature != none) { wrapper = _.text.FromStringM(commandLine); - commandsFeature.HandleInput(wrapper, callerPlayer); + feature.HandleInput(wrapper, instigator); _.memory.Free(wrapper); } + _.memory.Free(feature); } -/// Closes a command lock with a given case-insensitive name. -/// -/// Command locks are basically just boolean values that commands can use to check for whether they -/// are allowed to perform certain actions (e.g. cheats). -public final function bool Lock(BaseText lockName) { +/// Returns current status of a lock with a given name. +public final function LockStatus GetLockStatus(BaseText lockName) { + local LockStatus result; local Text lowerCaseName; - if (lockName == none) return false; - if (commandsFeature == none) return false; - + if (lockName == none) { + return LS_ClosedFixed; + } if (commandLocks == none) { commandLocks = _.collections.EmptyHashTable(); } lowerCaseName = lockName.LowerCopy(); - commandLocks.SetBool(lowerCaseName, true); + result = LockStatus(commandLocks.GetInt(lowerCaseName)); lowerCaseName.FreeSelf(); - return true; + return result; } -/// Opens a command lock with a given case-insensitive name. +/// Returns current status of a lock with a given name. +public final function LockStatus GetLockStatus_S(string lockName) { + local LockStatus result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(lockName); + result = GetLockStatus(wrapper); + _.memory.Free(wrapper); + return result; +} + +/// Sets new status for a lock with a given name. /// -/// Command locks are basically just boolean values that commands can use to check for whether they -/// are allowed to perform certain actions (e.g. cheats). -public final function bool Unlock(BaseText lockName) { +/// Can fail and return `false` in case lock was already fixed in the opposite +/// state. Otherwise returns `true`. +public final function bool SetLockStatus(BaseText lockName, LockStatus newStatus) { + local LockStatus previousStatus; local Text lowerCaseName; - if (lockName == none) return false; - if (commandsFeature == none) return false; - + if (lockName == none) { + return false; + } if (commandLocks == none) { commandLocks = _.collections.EmptyHashTable(); } lowerCaseName = lockName.LowerCopy(); - commandLocks.SetBool(lowerCaseName, false); + previousStatus = LockStatus(commandLocks.GetInt(lowerCaseName)); + if (previousStatus != LS_OpenFixed) { + commandLocks.SetInt(lowerCaseName, int(newStatus)); + lowerCaseName.FreeSelf(); + return true; + } lowerCaseName.FreeSelf(); - return true; + return false; +} + +/// Sets new status for a lock with a given name. +/// +/// Can fail and return `false` in case lock was already fixed in the opposite +/// state. Otherwise returns `true`. +public final function bool SetLockStatus_S(string lockName, LockStatus newStatus) { + local bool result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(lockName); + result = SetLockStatus(wrapper, newStatus); + _.memory.Free(wrapper); + return result; +} + +/// Closes a command lock with a given case-insensitive name, preventing it from +/// being opened again. +/// +/// Can fail and return `false` in case lock was already fixed in the opened +/// state. Otherwise returns `true`. +public final function bool Lock(BaseText lockName) { + return SetLockStatus(lockName, LS_ClosedFixed); +} + +/// Closes a command lock with a given case-insensitive name, preventing it from +/// being opened again. +/// +/// Can fail and return `false` in case lock was already fixed in the opened +/// state. Otherwise returns `true`. +public final function bool Lock_S(string lockName) { + return SetLockStatus_S(lockName, LS_ClosedFixed); +} + +/// Opens a command lock with a given case-insensitive name. +/// +/// Lock can still be closed after successful execution of this command. +/// +/// Can fail and return `false` in case lock was already fixed in the closed +/// state. Otherwise returns `true`. +public final function bool Unlock(BaseText lockName) { + return SetLockStatus(lockName, LS_Open); +} + +/// Opens a command lock with a given case-insensitive name. +/// +/// Lock can still be closed after successful execution of this command. +/// +/// Can fail and return `false` in case lock was already fixed in the closed +/// state. Otherwise returns `true`. +public final function bool Unlock_S(string lockName) { + return SetLockStatus_S(lockName, LS_Open); +} + +/// Opens a command lock with a given case-insensitive name, preventing it from +/// being closed again. +/// +/// Can fail and return `false` in case lock was already fixed in the closed +/// state. Otherwise returns `true`. +public final function bool BreakOpenLock(BaseText lockName) { + return SetLockStatus(lockName, LS_OpenFixed); +} + +/// Opens a command lock with a given case-insensitive name, preventing it from +/// being closed again. +/// +/// Can fail and return `false` in case lock was already fixed in the closed +/// state. Otherwise returns `true`. +public final function bool BreakOpenLock_S(string lockName) { + return SetLockStatus_S(lockName, LS_OpenFixed); } /// Checks if a command lock with a given case-insensitive name is closed. /// -/// Command locks are basically just boolean values that commands can use to check for whether they -/// are allowed to perform certain actions (e.g. cheats). +/// If lock is closed, method returns `true` (regardless of whether or not it is +/// fixed in closed state) and `false` if its currently open. public final function bool IsLocked(BaseText lockName) { + local LockStatus currentStatus; + currentStatus = GetLockStatus(lockName); + return (currentStatus == LS_ClosedFixed || currentStatus == LS_Closed); +} + +/// Checks if a command lock with a given case-insensitive name is closed. +/// +/// If lock is closed, method returns `true` (regardless of whether or not it is +/// fixed in closed state) and `false` if its currently open. +public final function bool IsLocked_S(string lockName) { + local LockStatus currentStatus; + currentStatus = GetLockStatus_S(lockName); + return (currentStatus == LS_ClosedFixed || currentStatus == LS_Closed); +} + +/// Returns array of names of all available commands. +/// +/// Resulting array can contain duplicates, but only if the same voting class +/// was registered by several different names. +public final function array< class > GetAllVotingClasses() { + local int i; + local array< class > intermediaryResult; + local array< class > result; + + if (VerifyVotingsTool()) { + intermediaryResult = votingsTool.GetAllItemClasses(); + for (i = 0; i < intermediaryResult.length; i += 1) { + result[result.length] = class(intermediaryResult[i]); + } + } + return result; +} + +/// Returns array of names of all available votings. +/// +/// Resulting array cannot contain duplicates. +public final function array GetAllVotingsNames() { + local array emptyResult; + + if (VerifyVotingsTool()) { + return votingsTool.GetItemsNames(); + } + return emptyResult; +} + +/// Registers given voting class, making it available via [`StartVoting()`]. +/// +/// Optionally a [`BaseText`] can be specified to be used as this voting's name. +/// If name parameter is omitted (specified as `none`) or constitutes an empty +/// [`BaseText`], a voting's preferred name will be used. +/// +/// If voting name is specified as `none`, then "default" will be +/// used instead. For non-`none` values, invalid name (according to +/// [`BaseText::IsValidName()`] method) will also prevent the group from being +/// authorized. +/// +/// # Errors +/// +/// Returns `true` if voting was successfully registered and `false` +/// otherwise`. +/// +/// If [`votingClass`] provides voting with a name that is already taken +/// (comparison is case-insensitive) by a different voting - a warning will be +/// logged and voting class won't be registered. +/// +/// If votingClass` provides voting with an empty name (and it isn't +/// overridden by the argument) - a warning will be logged and voting class +/// won't be registered. +public final function bool AddVoting( + class votingClass, + optional BaseText votingName +) { local bool result; - local Text lowerCaseName; + local Text immutableVotingName; - if (lockName == none) return true; - if (commandsFeature == none) return true; - if (commandLocks == none) return true; + if (votingClass == none) { + return false; + } + if (VerifyVotingsTool()) { + if (votingName != none) { + immutableVotingName = votingName.Copy(); + } else { + immutableVotingName = votingClass.static.GetPreferredName(); + } + result = votingsTool.AddItemClass(votingClass, immutableVotingName); + if (result) { + onVotingAddedSignal.Emit(votingClass, immutableVotingName); + } + _.memory.Free(immutableVotingName); + } + return false; +} - lowerCaseName = lockName.LowerCopy(); - result = commandLocks.GetBool(lowerCaseName); - lowerCaseName.FreeSelf(); +/// Registers given voting class, making it available via [`StartVoting()`]. +/// +/// Optionally a [`BaseText`] can be specified to be used as this voting's name. +/// If name parameter is omitted (specified as `none`) or constitutes an empty +/// [`BaseText`], a voting's preferred name will be used. +/// +/// Invalid name (according to [`BaseText::IsValidName()`] method) will prevent +/// the [`Voting`] from being authorized. +/// +/// # Errors +/// +/// Returns `true` if voting was successfully registered and `false` +/// otherwise`. +/// +/// If [`votingClass`] provides voting with a name that is already taken +/// (comparison is case-insensitive) by a different voting - a warning will be +/// logged and voting class won't be registered. +/// +/// If votingClass` provides voting with an empty name (and it isn't +/// overridden by the argument) - a warning will be logged and voting class +/// won't be registered. +public final function bool AddVoting_S( + class votingClass, + optional string votingName +) { + local bool result; + local MutableText wrapper; + + if (votingName != "") { + wrapper = _.text.FromStringM(votingName); + } + result = AddVoting(votingClass, wrapper); + _.memory.Free(wrapper); return result; } -/// Closes all command locks. +/// Registers given voting class, making it available via [`StartVoting()`]. +/// +/// Doesn't register voting immediately, instead scheduling it to be done at +/// a later moment in time. This can help to reduce amount of work we do every +/// tick during server startup, therefore avoiding crashed due to the faulty +/// infinite loop detection. +/// +/// Optionally a [`BaseText`] can be specified to be used as this voting's name. +/// If name parameter is omitted (specified as `none`) or constitutes an empty +/// [`BaseText`], a voting's preferred name will be used. +/// +/// If voting name is specified as `none`, then "default" will be +/// used instead. For non-`none` values, invalid name (according to +/// [`BaseText::IsValidName()`] method) will also prevent the group from being +/// authorized. +/// +/// # Errors +/// +/// Returns `true` if voting was successfully registered and `false` +/// otherwise`. +/// +/// If [`votingClass`] provides voting with a name that is already taken +/// (comparison is case-insensitive) by a different voting - a warning will be +/// logged and voting class won't be registered. +/// +/// If votingClass` provides voting with an empty name (and it isn't +/// overridden by the argument) - a warning will be logged and voting class +/// won't be registered. +public final function AddVotingAsync( + class votingClass, + optional BaseText votingName +) { + local AsyncTask newTask; + + if (!VerifyVotingsTool()) { + return; + } + newTask.type = CAJT_AddVoting; + newTask.entityClass = votingClass; + if (votingName != none) { + newTask.entityName = votingName.Copy(); + } + pendingAsyncJobs[pendingAsyncJobs.length] = newTask; + if (registeringJob == none || registeringJob.IsCompleted()) { + _.memory.Free(registeringJob); + registeringJob = CommandRegistrationJob(_.memory.Allocate(class'CommandRegistrationJob')); + _.scheduler.AddJob(registeringJob); + } +} + +/// Registers given voting class, making it available via [`StartVoting()`]. +/// +/// Doesn't register votings immediately, instead scheduling it to be done at +/// a later moment in time. This can help to reduce amount of work we do every +/// tick during server startup, therefore avoiding crashed due to the faulty +/// infinite loop detection. Different async calls from [`CommandAPI`] are +/// guaranteed to be handled in the order they were called. +/// +/// Optionally a [`BaseText`] can be specified to be used as this voting's name. +/// If name parameter is omitted (specified as `none`) or constitutes an empty +/// [`BaseText`], a voting's preferred name will be used. +/// +/// Invalid name (according to [`BaseText::IsValidName()`] method) will prevent +/// the [`Voting`] from being authorized. +/// +/// # Errors +/// +/// Returns `true` if voting was successfully registered and `false` +/// otherwise`. +/// +/// If [`votingClass`] provides voting with a name that is already taken +/// (comparison is case-insensitive) by a different voting - a warning will be +/// logged and voting class won't be registered. +/// +/// If votingClass` provides voting with an empty name (and it isn't +/// overridden by the argument) - a warning will be logged and voting class +/// won't be registered. +public final function AddVotingAsync_S( + class votingClass, + optional string votingName +) { + local MutableText wrapper; + + if (votingName != "") { + wrapper = _.text.FromStringM(votingName); + } + AddVotingAsync(votingClass, wrapper); + _.memory.Free(wrapper); +} + +/// Authorizes new user group to use the specified voting, optionally +/// specifying name of the config (config's class is determined by the +/// [`Voting`]'s class) that describes permissions of that group. +/// +/// Method must be called after [`Voting`] with a given name is added. +/// +/// If config name is specified as `none`, then "default" will be +/// used instead. For non-`none` values, invalid name (according to +/// [`BaseText::IsValidName()`] method) will also prevent the group from being +/// authorized. +/// +/// Function will return `true` if group was successfully authorized and +/// `false` otherwise (either group already authorized or no voting with +/// specified name was added in the caller tool). +/// +/// # Errors +/// +/// If specified group was already authorized to use a voting, then it +/// will log a warning message about it. +public function bool AuthorizeVotingUsage( + BaseText votingName, + BaseText groupName, + optional BaseText configName +) { + if (VerifyVotingsTool()) { + return votingsTool.AuthorizeUsage(votingName, groupName, configName); + } + return false; +} + +/// Authorizes new user group to use the specified voting, optionally +/// specifying name of the config (config's class is determined by the +/// [`Voting`]'s class) that describes permissions of that group. +/// +/// Method must be called after [`Voting`] with a given name is added. +/// +/// If this config name is specified as empty, then "default" will be +/// used instead. For non-empty values, an invalid name (according to +/// [`BaseText::IsValidName()`] method) will prevent the group from being +/// authorized. +/// +/// Function will return `true` if group was successfully authorized and +/// `false` otherwise (either group already authorized or no voting with +/// specified name was added in the caller tool). /// -/// Command locks are basically just boolean values that commands can use to check for whether they -/// are allowed to perform certain actions (e.g. cheats). -public final function CloseAllLocks() { - _.memory.Free(commandLocks); - commandLocks = none; +/// # Errors +/// +/// If specified group was already authorized to use a voting, then it +/// will log a warning message about it. +public function bool AuthorizeVotingUsage_S( + string votingName, + string userGroupName, + optional string configName +) { + local bool result; + local MutableText wrapperVotingName, wrapperGroupName, wrapperConfigName; + + wrapperVotingName = _.text.FromStringM(votingName); + wrapperGroupName = _.text.FromStringM(userGroupName); + wrapperConfigName = _.text.FromStringM(configName); + result = AuthorizeVotingUsage(wrapperVotingName, wrapperGroupName, wrapperConfigName); + _.memory.Free3(wrapperVotingName, wrapperGroupName, wrapperConfigName); + return result; +} + +/// Authorizes new user group to use the specified voting, optionally +/// specifying name of the config (config's class is determined by the +/// [`Voting`]'s class) that describes permissions of that group. +/// +/// Method must be called after [`Voting`] with a given name is added. +/// +/// Doesn't authorize votings immediately, instead scheduling it to be done at +/// a later moment in time. This can help to reduce amount of work we do every +/// tick during server startup, therefore avoiding crashed due to the faulty +/// infinite loop detection. Different async calls from [`CommandAPI`] are +/// guaranteed to be handled in the order they were called. +/// +/// If config name is specified as `none`, then "default" will be +/// used instead. For non-`none` values, invalid name (according to +/// [`BaseText::IsValidName()`] method) will also prevent the group from being +/// authorized. +/// +/// Function will return `true` if group was successfully authorized and +/// `false` otherwise (either group already authorized or no voting with +/// specified name was added in the caller tool). +/// +/// # Errors +/// +/// If specified group was already authorized to use a voting, then it +/// will log a warning message about it. +public function AuthorizeVotingUsageAsync( + BaseText votingName, + BaseText userGroupName, + optional BaseText configName +) { + local AsyncTask newTask; + + if (!VerifyVotingsTool()) { + return; + } + newTask.type = CAJT_AuthorizeVoting; + if (votingName != none) { + newTask.entityName = votingName.Copy(); + } + if (userGroupName != none) { + newTask.userGroup = userGroupName.Copy(); + } + if (configName != none) { + newTask.configName = configName.Copy(); + } + pendingAsyncJobs[pendingAsyncJobs.length] = newTask; + if (registeringJob == none || registeringJob.IsCompleted()) { + _.memory.Free(registeringJob); + registeringJob = CommandRegistrationJob(_.memory.Allocate(class'CommandRegistrationJob')); + _.scheduler.AddJob(registeringJob); + } +} + +/// Authorizes new user group to use the specified voting, optionally +/// specifying name of the config (config's class is determined by the +/// [`Voting`]'s class) that describes permissions of that group. +/// +/// Method must be called after [`Voting`] with a given name is added. +/// +/// Doesn't authorize votings immediately, instead scheduling it to be done at +/// a later moment in time. This can help to reduce amount of work we do every +/// tick during server startup, therefore avoiding crashed due to the faulty +/// infinite loop detection. Different async calls from [`CommandAPI`] are +/// guaranteed to be handled in the order they were called. +/// +/// If this config name is specified as empty, then "default" will be +/// used instead. For non-empty values, an invalid name (according to +/// [`BaseText::IsValidName()`] method) will prevent the group from being +/// authorized. +/// +/// Function will return `true` if group was successfully authorized and +/// `false` otherwise (either group already authorized or no voting with +/// specified name was added in the caller tool). +/// +/// # Errors +/// +/// If specified group was already authorized to use a voting, then it +/// will log a warning message about it. +public function AuthorizeVotingUsageAsync_S( + string votingName, + string userGroupName, + optional string configName +) { + local MutableText wrapperVotingName, wrapperGroupName, wrapperConfigName; + + wrapperVotingName = _.text.FromStringM(votingName); + wrapperGroupName = _.text.FromStringM(userGroupName); + wrapperConfigName = _.text.FromStringM(configName); + AuthorizeVotingUsageAsync(wrapperVotingName, wrapperGroupName, wrapperConfigName); + _.memory.Free3(wrapperVotingName, wrapperGroupName, wrapperConfigName); +} + +/// Removes voting of given class from the list of registered votings. +/// +/// Removing once registered votings 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 votings). +/// +/// Returns `true` if successfully removed registered [`Voting`] class and +/// `false` otherwise (voting wasn't registered). +public final function bool RemoveVoting(class votingClass) { + local bool result; + + if (VerifyVotingsTool()) { + result = votingsTool.RemoveItemClass(votingClass); + if (result) { + onVotingRemovedSignal.Emit(votingClass); + } + } + return result; +} + +/// Returns class of the [`Voting`] that was added under a specified name. +public final function class GetVotingClass(BaseText itemName) { + local ItemConfigInfo intermediaryResult; + + if (VerifyVotingsTool()) { + intermediaryResult = votingsTool.ResolveItem(itemName, none); + return class(intermediaryResult.class); + } + return none; +} + +/// Returns pair of [`Voting`] and config name based on a given +/// case-insensitive name and text ID of the caller player. +/// +/// Function only returns `none` for [`Voting`] instance if [`Voting`] with +/// a given name wasn't found. +public final function VotingConfigInfo ResolveVotingForTextID(BaseText itemName, BaseText id) { + local ItemConfigInfo intermediaryResult; + local VotingConfigInfo result; + + if (VerifyVotingsTool()) { + intermediaryResult = votingsTool.ResolveItem(itemName, id); + result.votingClass = class(intermediaryResult.class); + if (result.votingClass == none) { + return result; + } + if (intermediaryResult.configName == none) { + result.usageForbidden = true; + } else { + result.config = result.votingClass.static.LoadConfig(intermediaryResult.configName); + _.memory.Free(intermediaryResult.configName); + } + } + return result; +} + +/// Returns pair of [`Voting`] and config name based on a given +/// case-insensitive name and text ID of the caller player. +/// +/// Function only returns `none` for [`Voting`] instance if [`Voting`] with +/// a given name wasn't found. +public final function VotingConfigInfo ResolveVotingForTextID_S(string itemName, string id) { + local VotingConfigInfo result; + local MutableText wrapperItemname, wrapperID; + + wrapperItemname = _.text.FromStringM(itemName); + wrapperID = _.text.FromStringM(id); + result = ResolveVotingForTextID(wrapperItemname, wrapperID); + _.memory.Free2(wrapperItemname, wrapperID); + return result; +} + +/// Returns pair of [`Voting`] and config name based on a given +/// case-insensitive name and [`UserID`] of the caller player. +/// +/// Function only returns `none` for [`Voting`] instance if [`Voting`] with +/// a given name wasn't found or provided id was `none`. +public final function VotingConfigInfo ResolveVotingForUserID(BaseText itemName, UserID id) { + local VotingConfigInfo result; + local Text textID; + + if (itemName == none) return result; + if (id == none) return result; + textID = id.GetUniqueID(); + if (textID == none) return result; + + result = ResolveVotingForTextID(itemName, textID); + textID.FreeSelf(); + return result; +} + +/// Returns pair of [`Voting`] and config name based on a given +/// case-insensitive name and [`UserID`] of the caller player. +/// +/// Function only returns `none` for [`Voting`] instance if [`Voting`] with +/// a given name wasn't found or provided id was `none`. +public final function VotingConfigInfo ResolveVotingForUserID_S(string itemName, UserID id) { + local VotingConfigInfo result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(itemName); + result = ResolveVotingForUserID(wrapper, id); + _.memory.Free(wrapper); + return result; +} + +/// Returns pair of [`Voting`] and config name based on a given +/// case-insensitive name and [`User`] of the caller player. +/// +/// Function only returns `none` for [`Voting`] instance if [`Voting`] with +/// a given name wasn't found or provided id was `none`. +public final function VotingConfigInfo ResolveVotingForUser(BaseText itemName, User user) { + local VotingConfigInfo result; + local UserID id; + + if (itemName == none) return result; + if (user == none) return result; + + id = user.GetID(); + result = ResolveVotingForUserID(itemName, id); + _.memory.Free(id); + return result; +} + +/// Returns pair of [`Voting`] and config name based on a given +/// case-insensitive name and [`User`] of the caller player. +/// +/// Function only returns `none` for [`Voting`] instance if [`Voting`] with +/// a given name wasn't found or provided id was `none`. +public final function VotingConfigInfo ResolveVotingForUser_S(string itemName, User user) { + local VotingConfigInfo result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(itemName); + result = ResolveVotingForUser(wrapper, user); + _.memory.Free(wrapper); + return result; +} + +/// Starts a voting process with a given name and arguments. +public final function StartVotingResult StartVoting( + VotingConfigInfo votingData, + optional HashTable arguments) { + if (VerifyVotingsTool()) { + return votingsTool.StartVoting(votingData, arguments); + } + return SVR_InvalidState; +} + +/// Returns instance of the active voting. +/// +/// `none` iff no voting is currently active. +public final function Voting GetCurrentVoting() { + if (VerifyVotingsTool()) { + return votingsTool.GetCurrentVoting(); + } + return none; +} + +// DO NOT CALL MANUALLY +public final /*internal*/ function AsyncTask _popPending() { + local AsyncTask result; + + if (pendingAsyncJobs.length == 0) { + return result; + } + result = pendingAsyncJobs[0]; + pendingAsyncJobs.Remove(0, 1); + return result; +} + +// DO NOT CALL MANUALLY +public final /*internal*/ function _reloadFeature() { + local Commands_Feature commandsFeature; + local CommandFeatureTools toolsBundle; + + commandsToolLifeVersion = -1; + votingsToolLifeVersion = -1; + commandsFeature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); + if (commandsFeature != none) { + toolsBundle = commandsFeature._borrowTools(); + commandsTool = toolsBundle.commands; + if (commandsTool != none) { + commandsToolLifeVersion = commandsTool.GetLifeVersion(); + } + votingsTool = toolsBundle.votings; + if (votingsTool != none) { + votingsToolLifeVersion = votingsTool.GetLifeVersion(); + } + } + _.memory.Free(commandsFeature); +} + +private final function bool VerifyCommandsTool() { + if (commandsTool == none) { + return false; + } + if (!commandsTool.IsAllocated() || commandsTool.GetLifeVersion() != commandsToolLifeVersion) { + commandsTool = none; + return false; + } + return true; +} + +private final function bool VerifyVotingsTool() { + if (votingsTool == none) { + return false; + } + if (!votingsTool.IsAllocated() || votingsTool.GetLifeVersion() != votingsToolLifeVersion) { + votingsTool = none; + return false; + } + return true; } defaultproperties { diff --git a/sources/BaseAPI/API/Commands/CommandRegistrationJob.uc b/sources/BaseAPI/API/Commands/CommandRegistrationJob.uc index 5bc783c..89c3d09 100644 --- a/sources/BaseAPI/API/Commands/CommandRegistrationJob.uc +++ b/sources/BaseAPI/API/Commands/CommandRegistrationJob.uc @@ -19,36 +19,63 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class CommandRegistrationJob extends SchedulerJob; +class CommandRegistrationJob extends SchedulerJob + dependson(CommandAPI); -var private class nextCommand; +var private CommandAPI.AsyncTask nextItem; + +// Expecting 300 units of work, this gives us registering 20 commands per tick +const ADDING_COMMAND_COST = 15; +// Adding voting option is approximately the same as adding a command's +// single sub-command - we'll estimate it as 1/3rd of the full value +const ADDING_VOTING_COST = 5; +// Authorizing is relatively cheap, whether it's commands or voting +const AUTHORIZING_COST = 1; protected function Constructor() { - nextCommand = _.commands._popPending(); + nextItem = _.commands._popPending(); } protected function Finalizer() { - nextCommand = none; + _.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName); + nextItem.entityClass = none; + nextItem.entityName = none; + nextItem.userGroup = none; + nextItem.configName = none; } public function bool IsCompleted() { - return (nextCommand == none); + return (nextItem.entityName == none); } public function DoWork(int allottedWorkUnits) { - local int i, iterationsAmount; - - // Expected 300 units per tick, to register 20 commands per tick use about 10 - iterationsAmount = Max(allottedWorkUnits / 10, 1); - for (i = 0; i < iterationsAmount; i += 1) { - _.commands.RegisterCommand(nextCommand); - nextCommand = _.commands._popPending(); - if (nextCommand == none) { - break; + while (allottedWorkUnits > 0 && nextItem.entityName != none) { + if (nextItem.type == CAJT_AddCommand) { + allottedWorkUnits -= ADDING_COMMAND_COST; + _.commands.AddCommand(class(nextItem.entityClass), nextItem.entityName); + _.memory.Free(nextItem.entityName); + } else if (nextItem.type == CAJT_AddVoting) { + allottedWorkUnits -= ADDING_VOTING_COST; + _.commands.AddVoting(class(nextItem.entityClass), nextItem.entityName); + _.memory.Free(nextItem.entityName); + } else if (nextItem.type == CAJT_AuthorizeCommand) { + allottedWorkUnits -= AUTHORIZING_COST; + _.commands.AuthorizeCommandUsage( + nextItem.entityName, + nextItem.userGroup, + nextItem.configName); + _.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName); + } else /*if (nextItem.type == CAJT_AuthorizeVoting)*/ { + allottedWorkUnits -= AUTHORIZING_COST; + _.commands.AuthorizeVotingUsage( + nextItem.entityName, + nextItem.userGroup, + nextItem.configName); + _.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName); } + nextItem = _.commands._popPending(); } } -defaultproperties -{ +defaultproperties { } \ No newline at end of file diff --git a/sources/BaseAPI/API/Commands/Commands.uc b/sources/BaseAPI/API/Commands/Commands.uc index 82f5ba7..4fe1826 100644 --- a/sources/BaseAPI/API/Commands/Commands.uc +++ b/sources/BaseAPI/API/Commands/Commands.uc @@ -21,40 +21,210 @@ */ class Commands extends FeatureConfig perobjectconfig - config(AcediaSystem); + config(AcediaCommands); -var public config bool useChatInput; +/// Auxiliary struct for describing adding a particular command set to +/// a particular group of users. +struct CommandSetGroupPair { + /// Name of the command set to add + var public string name; + /// Name of the group, for which to add this set + var public string for; +}; + +/// Auxiliary struct for describing a rule to rename a particular command for +/// compatibility reasons. +struct RenamingRulePair { + /// Command class to rename + var public class rename; + /// Name to use for that class + var public string to; +}; + +/// Setting this to `true` enables players to input commands with "mutate" +/// console command. +/// Default is `true`. var public config bool useMutateInput; +/// Setting this to `true` enables players to input commands right in the chat +/// by prepending them with [`chatCommandPrefix`]. +/// Default is `true`. +var public config bool useChatInput; +/// Chat messages, prepended by this prefix will be treated as commands. +/// Default is "!". Empty values are also treated as "!". var public config string chatCommandPrefix; +/// Allows to specify which user groups are used in determining command/votings +/// permission. +/// They must be specified in the order of importance: from the group with +/// highest level of permissions to the lowest. When determining player's +/// permission to use a certain command/voting, his group with the highest +/// available permissions will be used. +var public config array commandGroup; +/// Add a specified `CommandList` to the specified user group +var public config array addCommandList; +/// Allows to specify a name for a certain command class +/// +/// NOTE:By default command choses that name by itself and its not recommended +/// to override it. You should only use this setting in case there is naming +/// conflict between commands from different packages. +var public config array renamingRule; +/// Allows to specify a name for a certain voting class +/// +/// NOTE:By default voting choses that name by itself and its not recommended +/// to override it. You should only use this setting in case there is naming +/// conflict between votings from different packages. +var public config array votingRenamingRule; protected function HashTable ToData() { + local int i; local HashTable data; + local ArrayList innerList; + local HashTable innerPair; data = __().collections.EmptyHashTable(); data.SetBool(P("useChatInput"), useChatInput, true); data.SetBool(P("useMutateInput"), useMutateInput, true); data.SetString(P("chatCommandPrefix"), chatCommandPrefix); + + // Serialize `commandGroup` + innerList = _.collections.EmptyArrayList(); + for (i = 0; i < commandGroup.length; i += 1) { + innerList.AddString(commandGroup[i]); + } + data.SetItem(P("commandGroups"), innerList); + _.memory.Free(innerList); + + // Serialize `addCommandSet` + innerList = _.collections.EmptyArrayList(); + for (i = 0; i < addCommandList.length; i += 1) { + innerPair = _.collections.EmptyHashTable(); + innerPair.SetString(P("name"), addCommandList[i].name); + innerPair.SetString(P("for"), addCommandList[i].for); + innerList.AddItem(innerPair); + _.memory.Free(innerPair); + } + data.SetItem(P("commandSets"), innerList); + _.memory.Free(innerList); + + // Serialize `renamingRule` + innerList = _.collections.EmptyArrayList(); + for (i = 0; i < renamingRule.length; i += 1) { + innerPair = _.collections.EmptyHashTable(); + innerPair.SetString(P("rename"), string(renamingRule[i].rename)); + innerPair.SetString(P("to"), renamingRule[i].to); + innerList.AddItem(innerPair); + _.memory.Free(innerPair); + } + data.SetItem(P("renamingRules"), innerList); + _.memory.Free(innerList); + + // Serialize `votingRenamingRule` + innerList = _.collections.EmptyArrayList(); + for (i = 0; i < votingRenamingRule.length; i += 1) { + innerPair = _.collections.EmptyHashTable(); + innerPair.SetString(P("rename"), string(votingRenamingRule[i].rename)); + innerPair.SetString(P("to"), votingRenamingRule[i].to); + innerList.AddItem(innerPair); + _.memory.Free(innerPair); + } + data.SetItem(P("votingRenamingRules"), innerList); + _.memory.Free(innerList); return data; } protected function FromData(HashTable source) { + local int i; + local ArrayList innerList; + local HashTable innerPair; + local CommandSetGroupPair nextCommandSetGroupPair; + local RenamingRulePair nextRenamingRule; + local class nextClass; + if (source == none) { return; } useChatInput = source.GetBool(P("useChatInput")); useMutateInput = source.GetBool(P("useMutateInput")); chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!"); + + // De-serialize `commandGroup` + commandGroup.length = 0; + innerList = source.GetArrayList(P("commandGroups")); + if (innerList != none) { + for (i = 0; i < commandGroup.length; i += 1) { + commandGroup[i] = innerList.GetString(i); + } + _.memory.Free(innerList); + } + + // De-serialize `addCommandSet` + addCommandList.length = 0; + innerList = source.GetArrayList(P("commandSets")); + if (innerList != none) { + for (i = 0; i < addCommandList.length; i += 1) { + innerPair = innerList.GetHashTable(i); + if (innerPair != none) { + nextCommandSetGroupPair.name = innerPair.GetString(P("name")); + nextCommandSetGroupPair.for = innerPair.GetString(P("for")); + addCommandList[addCommandList.length] = nextCommandSetGroupPair; + _.memory.Free(innerPair); + } + } + _.memory.Free(innerList); + } + + // De-serialize `renamingRule` + renamingRule.length = 0; + innerList = source.GetArrayList(P("renamingRules")); + if (innerList != none) { + for (i = 0; i < renamingRule.length; i += 1) { + innerPair = innerList.GetHashTable(i); + if (innerPair != none) { + nextClass = + class(_.memory.LoadClass_S(innerPair.GetString(P("rename")))); + nextRenamingRule.rename = nextClass; + nextRenamingRule.to = innerPair.GetString(P("to")); + renamingRule[renamingRule.length] = nextRenamingRule; + _.memory.Free(innerPair); + } + } + _.memory.Free(innerList); + } + + // De-serialize `votingRenamingRule` + votingRenamingRule.length = 0; + innerList = source.GetArrayList(P("votingRenamingRules")); + if (innerList != none) { + for (i = 0; i < votingRenamingRule.length; i += 1) { + innerPair = innerList.GetHashTable(i); + if (innerPair != none) { + nextClass = + class(_.memory.LoadClass_S(innerPair.GetString(P("rename")))); + nextRenamingRule.rename = nextClass; + nextRenamingRule.to = innerPair.GetString(P("to")); + votingRenamingRule[votingRenamingRule.length] = nextRenamingRule; + _.memory.Free(innerPair); + } + } + _.memory.Free(innerList); + } } protected function DefaultIt() { + local CommandSetGroupPair defaultPair; + useChatInput = true; useMutateInput = true; chatCommandPrefix = "!"; + commandGroup[0] = "admin"; + commandGroup[1] = "moderator"; + commandGroup[2] = "trusted"; + addCommandList.length = 0; + defaultPair.name = "default"; + defaultPair.for = "all"; + addCommandList[0] = defaultPair; + renamingRule.length = 0; } defaultproperties { - configName = "AcediaSystem" - useChatInput = true - useMutateInput = true - chatCommandPrefix = "!" + configName = "AcediaCommands" } \ No newline at end of file diff --git a/sources/BaseAPI/API/Commands/Commands_Feature.uc b/sources/BaseAPI/API/Commands/Commands_Feature.uc index df540a4..868ac0d 100644 --- a/sources/BaseAPI/API/Commands/Commands_Feature.uc +++ b/sources/BaseAPI/API/Commands/Commands_Feature.uc @@ -19,7 +19,9 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class Commands_Feature extends Feature; +class Commands_Feature extends Feature + dependson(CommandAPI) + dependson(Commands); //! This feature manages commands that automatically parse their arguments into standard Acedia //! collections. @@ -38,83 +40,81 @@ class Commands_Feature extends Feature; //! Emergency enabling this feature sets `emergencyEnabledMutate` flag that //! enforces connecting to the "mutate" input. -/// Pairs [`Voting`] class with a name its registered under in lower case for quick search. -struct NamedVoting { - var public class processClass; - /// Must be guaranteed to not be `none` and lower case as an invariant - var public Text processName; -}; - -/// Auxiliary struct for passing name of the command to call plus, optionally, additional +/// Auxiliary struct for passing name of the command to call with pre-specified /// sub-command name. /// -/// Normally sub-command name is parsed by the command itself, however command aliases can try to -/// enforce one. +/// Normally sub-command name is parsed by the command itself, however command +/// aliases can try to enforce one. struct CommandCallPair { var MutableText commandName; - /// In case it is enforced by an alias + /// Not `none` in case it is enforced by an alias var MutableText subCommandName; }; -/// Describes possible outcomes of starting a voting by its name -enum StartVotingResult { - /// Voting was successfully started - SVR_Success, - /// Voting wasn't started because another one was still in progress - SVR_AlreadyInProgress, - /// Voting wasn't started because voting with that name hasn't been registered - SVR_UnknownVoting +/// Auxiliary struct that stores all the information needed to load +/// a certain command +struct EntityLoadInfo { + /// Command class to load. + var public class entityClass; + /// Name to load that command class under. + var public Text name; + /// Groups that are authorized to use that command. + var public array authorizedGroups; + /// Groups that are authorized to use that command. + var public array groupsConfig; +}; + +/// Auxiliary struct for describing adding a particular command set to +/// a particular group of users. +struct CommandListGroupPair { + /// Name of the command set to add + var public Text commandListName; + /// Name of the group, for which to add this set + var public Text permissionGroup; +}; + +/// Auxiliary struct for describing a rule to rename a particular command for +/// compatibility reasons. +struct RenamingRulePair { + /// Command class to rename + var public class class; + /// Name to use for that class + var public Text newName; }; +/// Tools that provide functionality of managing registered commands and votings +var private CommandAPI.CommandFeatureTools tools; + /// Delimiters that always separate command name from it's parameters var private array commandDelimiters; -/// Registered commands, recorded as (, ) pairs. -/// Keys should be deallocated when their entry is removed. -var private HashTable registeredCommands; -/// [`HashTable`] of "" <-> [`ArrayList`] of commands pairs to allow quick fetch -/// of commands belonging to a single group -var private HashTable groupedCommands; - -/// [`Voting`]s that were already successfully loaded, ensuring that each has a unique name -var private array loadedVotings; - -/// Currently running voting process. -/// This feature doesn't actively track when voting ends, so reference can be non-`none` even if -/// voting has already ended. -var private Voting currentVoting; -/// An array of [`Voting`] objects that have been successfully loaded and -/// each object has a unique name. -var private array registeredVotings; - -/// When this flag is set to true, mutate input becomes available despite [`useMutateInput`] flag to -/// allow to unlock server in case of an error + +/// When this flag is set to true, mutate input becomes available despite +/// [`useMutateInput`] flag to allow to unlock server in case of an error var private bool emergencyEnabledMutate; -/// Setting this to `true` enables players to input commands right in the chat by prepending them -/// with [`chatCommandPrefix`]. -/// Default is `true`. var private /*config*/ bool useChatInput; -/// Setting this to `true` enables players to input commands with "mutate" console command. -/// Default is `true`. var private /*config*/ bool useMutateInput; -/// Chat messages, prepended by this prefix will be treated as commands. -/// Default is "!". Empty values are also treated as "!". var private /*config*/ Text chatCommandPrefix; - -var LoggerAPI.Definition errCommandDuplicate, errServerAPIUnavailable; -var LoggerAPI.Definition errVotingWithSameNameAlreadyRegistered, errYesNoVotingNamesReserved; +var public /*config*/ array commandGroup; +var public /*config*/ array addCommandSet; +var public /*config*/ array renamingRule; +var public /*config*/ array votingRenamingRule; + +// Converted version of `commandGroup` +var private array permissionGroupOrder; +/// Converted version of `addCommandSet` +var private array usedCommandLists; +/// Converted version of `renamingRule` and `votingRenamingRule` +var private array commandRenamingRules; +var private array votingRenamingRules; +// Name, under which `ACommandHelp` is registered +var private Text helpCommandName; + +var LoggerAPI.Definition errServerAPIUnavailable, warnDuplicateRenaming, warnNoCommandList; +var LoggerAPI.Definition infoCommandAdded, infoVotingAdded; protected function OnEnabled() { - registeredCommands = _.collections.EmptyHashTable(); - groupedCommands = _.collections.EmptyHashTable(); - RegisterCommand(class'ACommandHelp'); - RegisterCommand(class'ACommandNotify'); - RegisterCommand(class'ACommandVote'); - RegisterCommand(class'ACommandSideEffects'); - if (_.environment.IsDebugging()) { - RegisterCommand(class'ACommandFakers'); - } - RegisterVotingClass(class'Voting'); + helpCommandName = P("help"); // Macro selector commandDelimiters[0] = _.text.FromString("@"); // Key selector @@ -137,6 +137,16 @@ protected function OnEnabled() { _.logger.Auto(errServerAPIUnavailable); } } + LoadConfigArrays(); + // `SetPermissionGroupOrder()` must be called *after* loading configs + tools.commands = CommandsTool(_.memory.Allocate(class'CommandsTool')); + tools.votings = VotingsTool(_.memory.Allocate(class'VotingsTool')); + tools.commands.SetPermissionGroupOrder(permissionGroupOrder); + tools.votings.SetPermissionGroupOrder(permissionGroupOrder); + _.commands._reloadFeature(); + // Uses `CommandAPI`, so must be done after `_reloadFeature()` call + LoadCommands(); + LoadVotings(); } protected function OnDisabled() { @@ -146,18 +156,27 @@ protected function OnDisabled() { if (useMutateInput && __server() != none) { __server().unreal.mutator.OnMutate(self).Disconnect(); } + useChatInput = false; useMutateInput = false; - _.memory.Free3(registeredCommands, groupedCommands, chatCommandPrefix); - registeredCommands = none; - groupedCommands = none; + _.memory.Free3(tools.commands, tools.votings, chatCommandPrefix); + tools.commands = none; + tools.votings = none; chatCommandPrefix = none; + + _.memory.FreeMany(commandDelimiters); commandDelimiters.length = 0; - ReleaseNameVotingsArray(/*out*/ registeredVotings); + + _.memory.FreeMany(permissionGroupOrder); + permissionGroupOrder.length = 0; + + FreeUsedCommandSets(); + FreeRenamingRules(); + + _.commands._reloadFeature(); } -protected function SwapConfig(FeatureConfig config) -{ +protected function SwapConfig(FeatureConfig config) { local Commands newConfig; newConfig = Commands(config); @@ -168,10 +187,14 @@ protected function SwapConfig(FeatureConfig config) chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix); useChatInput = newConfig.useChatInput; useMutateInput = newConfig.useMutateInput; + commandGroup = newConfig.commandGroup; + addCommandSet = newConfig.addCommandList; + renamingRule = newConfig.renamingRule; + votingRenamingRule = newConfig.votingRenamingRule; } -/// This method allows to forcefully enable `Command_Feature` along with "mutate" input in case -/// something goes wrong. +/// This method allows to forcefully enable `Command_Feature` along with +/// "mutate" input in case something goes wrong. /// /// `Command_Feature` is a critical command to have running on your server and, /// if disabled by accident, there will be no way of starting it again without @@ -188,7 +211,7 @@ public final static function EmergencyEnable() { } feature = Commands_Feature(GetEnabledInstance()); noWayToInputCommands = !feature.emergencyEnabledMutate - &&!feature.IsUsingMutateInput() + && !feature.IsUsingMutateInput() && !feature.IsUsingChatInput(); if (noWayToInputCommands) { default.emergencyEnabledMutate = true; @@ -229,7 +252,8 @@ public final static function bool IsUsingMutateInput() { return false; } -/// Returns prefix that will indicate that chat message is intended to be a command. By default "!". +/// Returns prefix that will indicate that chat message is intended to be +/// a command. By default "!". /// /// If `Commands_Feature` is disabled, always returns `none`. public final static function Text GetChatPrefix() { @@ -242,309 +266,34 @@ public final static function Text GetChatPrefix() { return none; } -/// Returns `true` iff some voting is currently active. -public final function bool IsVotingRunning() { - if (currentVoting != none && currentVoting.HasEnded()) { - _.memory.Free(currentVoting); - currentVoting = none; - } - return (currentVoting != none); -} - -/// Returns instance of the active voting. -/// -/// `none` iff no voting is currently active. -public final function Voting GetCurrentVoting() { - if (currentVoting != none && currentVoting.HasEnded()) { - _.memory.Free(currentVoting); - currentVoting = none; - } - if (currentVoting != none) { - currentVoting.NewRef(); - } - return currentVoting; -} - -/// `true` if voting under the given name (case-insensitive) is already registered. -public final function bool IsVotingRegistered(BaseText processName) { - local int i; - - for (i = 0; i < registeredVotings.length; i += 1) { - if (processName.Compare(registeredVotings[i].processName, SCASE_INSENSITIVE)) { - return true; - } - } - return false; -} - -/// Returns class of the [`Voting`] registered under given name. -public final function StartVotingResult StartVoting(BaseText processName) { - local int i; - local Text votingSettingsName; - local class processClass; - - if (currentVoting != none && currentVoting.HasEnded()) { - _.memory.Free(currentVoting); - currentVoting = none; - } - if (currentVoting != none) { - return SVR_AlreadyInProgress; - } - for (i = 0; i < registeredVotings.length; i += 1) { - if (processName.Compare(registeredVotings[i].processName, SCASE_INSENSITIVE)) { - processClass = registeredVotings[i].processClass; - } - } - if (processClass == none) { - return SVR_UnknownVoting; - } - currentVoting = Voting(_.memory.Allocate(processClass)); - currentVoting.Start(votingSettingsName); - return SVR_Success; -} - -/// Registers a new voting class to be accessible through the [`Commands_Feature`]. -/// -/// When a voting class is registered, players can access it using the standard AcediaCore's "vote" -/// command. -/// However, note that registering a voting class is not mandatory for it to be usable. -/// In fact, if you want to prevent players from initiating a particular voting, you should avoid -/// registering it in this feature. -public final function RegisterVotingClass(class newVotingClass) { - local int i; - local ACommandVote votingCommand; - local NamedVoting newRecord; - local Text votingName; - - if (newVotingClass == none) return; - votingCommand = GetVotingCommand(); - if (votingCommand == none) return; - // We can freely release this reference here, since another reference is guaranteed to be kept in registered command - _.memory.Free(votingCommand); - // But just to make sure - if (!votingCommand.IsAllocated()) return; - - // First we check whether we already added this class - for (i = 0; i < registeredVotings.length; i += 1) { - if (registeredVotings[i].processClass == newVotingClass) { - return; - } - } - votingName = newVotingClass.static.GetPreferredName(); - if (votingName.Compare(P("yes")) || votingName.Compare(P("no"))) { - _.logger.Auto(errYesNoVotingNamesReserved).ArgClass(newVotingClass).Arg(votingName); - return; - } - // Check for duplicates - for (i = 0; i < registeredVotings.length; i += 1) { - if (registeredVotings[i].processName.Compare(votingName)) { - _.logger - .Auto(errVotingWithSameNameAlreadyRegistered) - .ArgClass(newVotingClass) - .Arg(votingName) - .ArgClass(registeredVotings[i].processClass); - return; - } - } - newRecord.processClass = newVotingClass; - newRecord.processName = votingName; - registeredVotings[registeredVotings.length] = newRecord; - votingCommand.AddVotingInfo(votingName, newVotingClass); -} - -/// Unregisters a voting class from the [`Commands_Feature`], preventing players from accessing it -/// through the standard AcediaCore "vote" command. -/// -/// This method does not stop any existing voting processes associated with the unregistered class. -/// -/// Use this method to remove a voting class that is no longer needed or to prevent players from -/// initiating a particular voting. Note that removing a voting class is a linear operation that may -/// take some time if many votings are currently registered. It is not expected to be a common -/// operation and should be used sparingly. -public final function RemoveVotingClass(class newVotingClass) { - local int i; - local ACommandVote votingCommand; - - if (newVotingClass == none) { - return; - } - for (i = 0; i < registeredVotings.length; i += 1) { - if (registeredVotings[i].processClass == newVotingClass) { - _.memory.Free(registeredVotings[i].processName); - registeredVotings.Remove(i, 1); - } - } - votingCommand = GetVotingCommand(); - if (votingCommand == none) { - return; - } - // Simply rebuild the whole voting set from scratch - votingCommand.ResetVotingInfo(); - for (i = 0; i < registeredVotings.length; i += 1) { - votingCommand.AddVotingInfo( - registeredVotings[i].processName, - registeredVotings[i].processClass); - } - _.memory.Free(votingCommand); -} - -/// Registers given command class, making it available. -/// -/// # Errors -/// -/// Returns `true` if command was successfully registered and `false` otherwise`. -/// -/// If `commandClass` provides command with a name that is already taken -/// (comparison is case-insensitive) by a different command - a warning will be -/// logged and newly passed `commandClass` discarded. -public final function bool RegisterCommand(class commandClass) { - local Text commandName, groupName; - local ArrayList groupArray; - local Command newCommandInstance, existingCommandInstance; - - if (commandClass == none) return false; - if (registeredCommands == none) return false; - - newCommandInstance = Command(_.memory.Allocate(commandClass, true)); - commandName = newCommandInstance.GetName(); - groupName = newCommandInstance.GetGroupName(); - // Check for duplicates and report them - existingCommandInstance = Command(registeredCommands.GetItem(commandName)); - if (existingCommandInstance != none) { - _.logger.Auto(errCommandDuplicate) - .ArgClass(existingCommandInstance.class) - .Arg(commandName) - .ArgClass(commandClass); - _.memory.Free(groupName); - _.memory.Free(newCommandInstance); - _.memory.Free(existingCommandInstance); - return false; - } - // Otherwise record new command - // `commandName` used as a key, do not deallocate it - registeredCommands.SetItem(commandName, newCommandInstance); - // Add to grouped collection - groupArray = groupedCommands.GetArrayList(groupName); - if (groupArray == none) { - groupArray = _.collections.EmptyArrayList(); - } - groupArray.AddItem(newCommandInstance); - groupedCommands.SetItem(groupName, groupArray); - _.memory.Free4(groupArray, groupName, commandName, newCommandInstance); - return true; -} - -/// Removes command of given class from the list of registered commands. -/// -/// Removing once registered commands is not an action that is expected to be performed under normal -/// circumstances and it is not efficient. -/// It is linear on the current amount of commands. -public final function RemoveCommand(class commandClass) { - local int i; - local CollectionIterator iter; - local Command nextCommand; - local Text nextCommandName; - local array commandGroup; - local array keysToRemove; - - if (commandClass == none) return; - if (registeredCommands == none) return; - - for (iter = registeredCommands.Iterate(); !iter.HasFinished(); iter.Next()) { - nextCommand = Command(iter.Get()); - nextCommandName = Text(iter.GetKey()); - if (nextCommand == none || nextCommandName == none || nextCommand.class != commandClass) { - _.memory.Free2(nextCommand, nextCommandName); - continue; - } - keysToRemove[keysToRemove.length] = nextCommandName; - commandGroup[commandGroup.length] = nextCommand.GetGroupName(); - _.memory.Free(nextCommand); - } - iter.FreeSelf(); - for (i = 0; i < keysToRemove.length; i += 1) { - registeredCommands.RemoveItem(keysToRemove[i]); - _.memory.Free(keysToRemove[i]); - } - for (i = 0; i < commandGroup.length; i += 1) { - RemoveClassFromGroup(commandClass, commandGroup[i]); - } - _.memory.FreeMany(commandGroup); -} - -/// Returns command based on a given name. +/// Returns name, under which [`ACommandHelp`] is registered. /// -/// Name of the registered `Command` to return is case-insensitive. -/// -/// If no command with such name was registered - returns `none`. -public final function Command GetCommand(BaseText commandName) { - local Text commandNameLowerCase; - local Command commandInstance; - - if (commandName == none) return none; - if (registeredCommands == none) return none; - - commandNameLowerCase = commandName.LowerCopy(); - commandInstance = Command(registeredCommands.GetItem(commandNameLowerCase)); - commandNameLowerCase.FreeSelf(); - return commandInstance; -} - -/// Returns array of names of all available commands. -public final function array GetCommandNames() { - local array emptyResult; - - if (registeredCommands != none) { - return registeredCommands.GetTextKeys(); - } - return emptyResult; -} - -/// Returns array of names of all available commands belonging to the group [`groupName`]. -public final function array GetCommandNamesInGroup(BaseText groupName) { - local int i; - local ArrayList groupArray; - local Command nextCommand; - local array result; - - if (groupedCommands == none) return result; - groupArray = groupedCommands.GetArrayList(groupName); - if (groupArray == none) return result; - - for (i = 0; i < groupArray.GetLength(); i += 1) { - nextCommand = Command(groupArray.GetItem(i)); - if (nextCommand != none) { - result[result.length] = nextCommand.GetName(); - } - _.memory.Free(nextCommand); - } - return result; -} - -/// Returns all available command groups' names. -public final function array GetGroupsNames() { - local array emptyResult; +/// If `Commands_Feature` is disabled, always returns `none`. +public final static function Text GetHelpCommandName() { + local Commands_Feature instance; - if (groupedCommands != none) { - return groupedCommands.GetTextKeys(); + instance = Commands_Feature(GetEnabledInstance()); + if (instance != none && instance.helpCommandName != none) { + return instance.helpCommandName.Copy(); } - return emptyResult; + return none; } /// Executes command based on the input. /// -/// Takes [`commandLine`] as input with command's call, finds appropriate registered command -/// instance and executes it with parameters specified in the [`commandLine`]. +/// Takes [`commandLine`] as input with command's call, finds appropriate +/// registered command instance and executes it with parameters specified in +/// the [`commandLine`]. /// -/// [`callerPlayer`] has to be specified and represents instigator of this command that will receive -/// appropriate result/error messages. +/// [`callerPlayer`] has to be specified and represents instigator of this +/// command that will receive appropriate result/error messages. /// /// Returns `true` iff command was successfully executed. /// /// # Errors /// -/// Doesn't log any errors, but can complain about errors in name or parameters to -/// the [`callerPlayer`] +/// Doesn't log any errors, but can complain about errors in name or parameters +/// to the [`callerPlayer`] public final function bool HandleInput(BaseText input, EPlayer callerPlayer) { local bool result; local Parser wrapper; @@ -560,21 +309,23 @@ public final function bool HandleInput(BaseText input, EPlayer callerPlayer) { /// Executes command based on the input. /// -/// Takes [`commandLine`] as input with command's call, finds appropriate registered command -/// instance and executes it with parameters specified in the [`commandLine`]. +/// Takes [`commandLine`] as input with command's call, finds appropriate +/// registered command instance and executes it with parameters specified in +/// the [`commandLine`]. /// -/// [`callerPlayer`] has to be specified and represents instigator of this command that will receive -/// appropriate result/error messages. +/// [`callerPlayer`] has to be specified and represents instigator of this +/// command that will receive appropriate result/error messages. /// /// Returns `true` iff command was successfully executed. /// /// # Errors /// -/// Doesn't log any errors, but can complain about errors in name or parameters to -/// the [`callerPlayer`] +/// Doesn't log any errors, but can complain about errors in name or parameters +/// to the [`callerPlayer`] public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) { local bool errorOccured; - local Command commandInstance; + local User identity; + local CommandAPI.CommandConfigInfo commandPair; local Command.CallData callData; local CommandCallPair callPair; @@ -582,34 +333,25 @@ public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) if (callerPlayer == none) return false; if (!parser.Ok()) return false; + identity = callerPlayer.GetIdentity(); callPair = ParseCommandCallPairWith(parser); - commandInstance = GetCommand(callPair.commandName); - if (commandInstance == none && callerPlayer != none && callerPlayer.IsExistent()) { - callerPlayer - .BorrowConsole() - .Flush() - .Say(F("{$TextFailure Command not found!}")); - } - if (parser.Ok() && commandInstance != none) { - callData = commandInstance.ParseInputWith(parser, callerPlayer, callPair.subCommandName); - errorOccured = commandInstance.Execute(callData, callerPlayer); - commandInstance.DeallocateCallData(callData); - } - _.memory.Free2(callPair.commandName, callPair.subCommandName); - return errorOccured; -} - -private final function ACommandVote GetVotingCommand() { - local AcediaObject registeredAsVote; - - if (registeredCommands != none) { - registeredAsVote = registeredCommands.GetItem(P("vote")); - if (registeredAsVote != none && registeredAsVote.class == class'ACommandVote') { - return ACommandVote(registeredAsVote); + commandPair = _.commands.ResolveCommandForUser(callPair.commandName, identity); + if (commandPair.instance == none || commandPair.usageForbidden) { + if (callerPlayer != none && callerPlayer.IsExistent()) { + callerPlayer + .BorrowConsole() + .Flush() + .Say(F("{$TextFailure Command not found!}")); } - _.memory.Free(registeredAsVote); } - return none; + if (parser.Ok() && commandPair.instance != none && !commandPair.usageForbidden) { + callData = + commandPair.instance.ParseInputWith(parser, callerPlayer, callPair.subCommandName); + errorOccured = commandPair.instance.Execute(callData, callerPlayer, commandPair.config); + commandPair.instance.DeallocateCallData(callData); + } + _.memory.Free4(commandPair.instance, callPair.commandName, callPair.subCommandName, identity); + return errorOccured; } // Parses command's name into `CommandCallPair` - sub-command is filled in case @@ -677,44 +419,290 @@ private function HandleMutate(string command, PlayerController sendingPlayer) { parser.FreeSelf(); } -private final function RemoveClassFromGroup(class commandClass, BaseText commandGroup) { +private final function LoadConfigArrays() { local int i; - local ArrayList groupArray; - local Command nextCommand; + local CommandListGroupPair nextCommandSetGroupPair; - groupArray = groupedCommands.GetArrayList(commandGroup); - if (groupArray == none) { - return; + for (i = 0; i < commandGroup.length; i += 1) { + permissionGroupOrder[i] = _.text.FromString(commandGroup[i]); + } + for (i = 0; i < addCommandSet.length; i += 1) { + nextCommandSetGroupPair.commandListName = _.text.FromString(addCommandSet[i].name); + nextCommandSetGroupPair.permissionGroup = _.text.FromString(addCommandSet[i].for); + usedCommandLists[i] = nextCommandSetGroupPair; + } + FreeRenamingRules(); + commandRenamingRules = LoadRenamingRules(renamingRule); + votingRenamingRules = LoadRenamingRules(votingRenamingRule); +} + +private final function array LoadRenamingRules( + array inputRules) { + local int i, j; + local RenamingRulePair nextRule; + local array result; + + // Clear away duplicates + for (i = 0; i < inputRules.length; i += 1) { + j = i + 1; + while (j < inputRules.length) { + if (inputRules[i].rename == inputRules[j].rename) { + _.logger.Auto(warnDuplicateRenaming) + .ArgClass(inputRules[i].rename) + .Arg(_.text.FromString(inputRules[i].to)) + .Arg(_.text.FromString(inputRules[j].to)); + inputRules.Remove(j, 1); + } else { + j += 1; + } + } + } + // Translate rules + for (i = 0; i < inputRules.length; i += 1) { + nextRule.class = inputRules[i].rename; + nextRule.newName = _.text.FromString(inputRules[i].to); + if (nextRule.class == class'ACommandHelp') { + _.memory.Free(helpCommandName); + helpCommandName = nextRule.newName.Copy(); + } + result[result.length] = nextRule; + } + return result; +} + +private final function LoadCommands() { + local int i, j; + local Text nextName; + local array commandClassesToLoad; + + commandClassesToLoad = CollectAllCommandClasses(); + // Load command names to use, according to preferred names and name rules + for (i = 0; i < commandClassesToLoad.length; i += 1) { + nextName = none; + for (j = 0; j < commandRenamingRules.length; j += 1) { + if (commandClassesToLoad[i].entityClass == commandRenamingRules[j].class) { + nextName = commandRenamingRules[j].newName.Copy(); + break; + } + } + if (nextName == none) { + nextName = class(commandClassesToLoad[i].entityClass) + .static.GetPreferredName(); + } + commandClassesToLoad[i].name = nextName; + } + // Actually load commands + for (i = 0; i < commandClassesToLoad.length; i += 1) { + _.commands.AddCommandAsync( + class(commandClassesToLoad[i].entityClass), + commandClassesToLoad[i].name); + for (j = 0; j < commandClassesToLoad[i].authorizedGroups.length; j += 1) { + _.commands.AuthorizeCommandUsageAsync( + commandClassesToLoad[i].name, + commandClassesToLoad[i].authorizedGroups[j], + commandClassesToLoad[i].groupsConfig[j]); + } + _.logger.Auto(infoCommandAdded) + .ArgClass(commandClassesToLoad[i].entityClass) + .Arg(/*take*/ commandClassesToLoad[i].name); + } + for (i = 0; i < commandClassesToLoad.length; i += 1) { + // `name` field was already released through `Arg()` logger function + _.memory.FreeMany(commandClassesToLoad[i].authorizedGroups); + _.memory.FreeMany(commandClassesToLoad[i].groupsConfig); + } +} + +private final function LoadVotings() { + local int i, j; + local Text nextName; + local array votingClassesToLoad; + + votingClassesToLoad = CollectAllVotingClasses(); + // Load voting names to use, according to preferred names and name rules + for (i = 0; i < votingClassesToLoad.length; i += 1) { + nextName = none; + for (j = 0; j < votingRenamingRules.length; j += 1) { + if (votingClassesToLoad[i].entityClass == votingRenamingRules[j].class) { + nextName = votingRenamingRules[j].newName.Copy(); + break; + } + } + if (nextName == none) { + nextName = class(votingClassesToLoad[i].entityClass) + .static.GetPreferredName(); + } + votingClassesToLoad[i].name = nextName; + } + // Actually load votings + for (i = 0; i < votingClassesToLoad.length; i += 1) { + _.commands.AddVotingAsync( + class(votingClassesToLoad[i].entityClass), + votingClassesToLoad[i].name); + for (j = 0; j < votingClassesToLoad[i].authorizedGroups.length; j += 1) { + _.commands.AuthorizeVotingUsageAsync( + votingClassesToLoad[i].name, + votingClassesToLoad[i].authorizedGroups[j], + votingClassesToLoad[i].groupsConfig[j]); + } + _.logger.Auto(infoVotingAdded) + .ArgClass(votingClassesToLoad[i].entityClass) + .Arg(/*take*/ votingClassesToLoad[i].name); + } + for (i = 0; i < votingClassesToLoad.length; i += 1) { + // `name` field was already released through `Arg()` logger function + _.memory.FreeMany(votingClassesToLoad[i].authorizedGroups); + _.memory.FreeMany(votingClassesToLoad[i].groupsConfig); + } +} + +// Guaranteed to not return `none` items in the array +private final function array CollectAllCommandClasses() { + local int i; + local bool debugging; + local CommandList nextList; + local array result; + + debugging = _.environment.IsDebugging(); + class'CommandList'.static.Initialize(); + for (i = 0; i < usedCommandLists.length; i += 1) { + nextList = CommandList(class'CommandList'.static + .GetConfigInstance(usedCommandLists[i].commandListName)); + if (nextList != none) { + if (!debugging && nextList.debugOnly) { + continue; + } + MergeEntityClassArrays( + result, + /*take*/ nextList.GetCommandData(), + usedCommandLists[i].permissionGroup); + } else { + _.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy()); + } } - while (i < groupArray.GetLength()) { - nextCommand = Command(groupArray.GetItem(i)); - if (nextCommand != none && nextCommand.class == commandClass) { - groupArray.RemoveIndex(i); + return result; +} + +// Guaranteed to not return `none` items in the array +private final function array CollectAllVotingClasses() { + local int i; + local bool debugging; + local CommandList nextList; + local array result; + + debugging = _.environment.IsDebugging(); + class'CommandList'.static.Initialize(); + for (i = 0; i < usedCommandLists.length; i += 1) { + nextList = CommandList(class'CommandList'.static + .GetConfigInstance(usedCommandLists[i].commandListName)); + if (nextList != none) { + if (!debugging && nextList.debugOnly) { + continue; + } + MergeEntityClassArrays( + result, + /*take*/ nextList.GetVotingData(), + usedCommandLists[i].permissionGroup); } else { - i += 1; + _.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy()); } - _.memory.Free(nextCommand); } - if (groupArray.GetLength() == 0) { - groupedCommands.RemoveItem(commandGroup); + return result; +} + +// Adds `newCommands` into `infoArray`, adding `commandsGroup` to +// their array `authorizedGroups` +// +// Assumes that all arguments aren't `none`. +// +// Guaranteed to not add `none` commands from `newCommands`. +// +// Assumes that items from `infoArray` all have `name` field set to `none`, +// will also leave them as `none`. +private final function MergeEntityClassArrays( + out array infoArray, + /*take*/ array newCommands, + Text commandsGroup +) { + local int i, infoToEditIndex; + local EntityLoadInfo infoToEdit; + local array editedArray; + + for (i = 0; i < newCommands.length; i += 1) { + if (newCommands[i].class == none) { + continue; + } + // Search for an existing record of class `newCommands[i]` in + // `infoArray`. If found, copy to `infoToEdit` and index into + // `infoToEditIndex`, else `infoToEditIndex` will hold the next unused + // index in `infoArray`. + infoToEditIndex = 0; + while (infoToEditIndex < infoArray.length) { + if (infoArray[infoToEditIndex].entityClass == newCommands[i].class) { + infoToEdit = infoArray[infoToEditIndex]; + break; + } + infoToEditIndex += 1; + } + // Update data inside `infoToEdit`. + infoToEdit.entityClass = newCommands[i].class; + + editedArray = infoToEdit.authorizedGroups; + editedArray[editedArray.length] = commandsGroup.Copy(); + infoToEdit.authorizedGroups = editedArray; + + editedArray = infoToEdit.groupsConfig; + editedArray[editedArray.length] = newCommands[i].config; // moving here + infoToEdit.groupsConfig = editedArray; + // Update `infoArray` with the modified record. + infoArray[infoToEditIndex] = infoToEdit; + // Forget about data `authorizedGroups` and `groupsConfig`: + // + // 1. Their references have already been moved into `infoArray` and + // don't need to be released; + // 2. If we don't find corresponding struct inside `infoArray` on + // the next iteration, we'll override `commandClass`, + // but not `authorizedGroups`/`groupsConfig`, so we'll just reset them + // to empty now. + // (`name` field is expected to be `none` during this method) + infoToEdit.authorizedGroups.length = 0; + infoToEdit.groupsConfig.length = 0; } - _.memory.Free(groupArray); } -private final function ReleaseNameVotingsArray(out array toRelease) { +private final function FreeUsedCommandSets() { local int i; - for (i = 0; i < toRelease.length; i += 1) { - _.memory.Free(toRelease[i].processName); - toRelease[i].processName = none; + for (i = 0; i < usedCommandLists.length; i += 1) { + _.memory.Free(usedCommandLists[i].commandListName); + _.memory.Free(usedCommandLists[i].permissionGroup); } - toRelease.length = 0; + usedCommandLists.length = 0; +} + +private final function FreeRenamingRules() { + local int i; + + for (i = 0; i < commandRenamingRules.length; i += 1) { + _.memory.Free(commandRenamingRules[i].newName); + } + commandRenamingRules.length = 0; + + for (i = 0; i < votingRenamingRules.length; i += 1) { + _.memory.Free(votingRenamingRules[i].newName); + } + votingRenamingRules.length = 0; +} + +public final function CommandAPI.CommandFeatureTools _borrowTools() { + return tools; } defaultproperties { configClass = class'Commands' - errCommandDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Command `%3` with the same name will be ignored.") errServerAPIUnavailable = (l=LOG_Error,m="Server API is currently unavailable. Input methods through chat and mutate command will not be available.") - errVotingWithSameNameAlreadyRegistered = (l=LOG_Error,m="Attempting to register voting process with class `%1` under the name \"%2\" when voting process `%3` is already registered. This is likely caused by conflicting mods.") - errYesNoVotingNamesReserved = (l=LOG_Error,m="Attempting to register voting process with class `%1` under the reserved name \"%2\". This is an issue with the mod that provided the voting, please contact its author.") + warnDuplicateRenaming = (l=LOG_Warning,m="Class `%1` has duplicate renaming rules: \"%2\" and \"%3\". Latter will be discarded. It is recommended that you fix your `AcediaCommands` configuration file.") + warnNoCommandList = (l=LOG_Warning,m="Command list \"%1\" not found.") + infoCommandAdded = (l=LOG_Info,m="`Commands_Feature` has resolved to load command `%1` as \"%2\".") + infoVotingAdded = (l=LOG_Info,m="`Commands_Feature` has resolved to load voting `%1` as \"%2\".") } \ No newline at end of file