/** * Author: dkanus * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore * License: GPL * Copyright 2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * * Acedia is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3 of the License, or * (at your option) any later version. * * Acedia is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ class CommandAPI extends AcediaObject; /// 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. var private HashTable commandLocks; var private CommandsTool commandsTool; var private VotingsTool votingsTool; var private int commandsToolLifeVersion; var private int votingsToolLifeVersion; 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 (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); } 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; } /// 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); } } /// 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); } /// 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 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 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; } /// 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. /// /// 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( BaseText commandName, BaseText userGroupName, optional BaseText configName ) { local AsyncTask newTask; if (!VerifyCommandsTool()) { return; } 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')); _.scheduler.AddJob(registeringJob); } } /// 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 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; } /// 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. /// /// 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. /// /// 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. /// /// 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. /// /// 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. /// /// 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; } /// 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. /// /// 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 [`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 [`instigator`]. public final function Execute_S(string commandLine, EPlayer instigator) { local MutableText wrapper; local Commands_Feature feature; feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); if (feature != none) { wrapper = _.text.FromStringM(commandLine); feature.HandleInput(wrapper, instigator); _.memory.Free(wrapper); } _.memory.Free(feature); } /// 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 LS_ClosedFixed; } if (commandLocks == none) { commandLocks = _.collections.EmptyHashTable(); } lowerCaseName = lockName.LowerCopy(); result = LockStatus(commandLocks.GetInt(lowerCaseName)); lowerCaseName.FreeSelf(); return result; } /// 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. /// /// 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 (commandLocks == none) { commandLocks = _.collections.EmptyHashTable(); } lowerCaseName = lockName.LowerCopy(); previousStatus = LockStatus(commandLocks.GetInt(lowerCaseName)); if (previousStatus != LS_OpenFixed) { commandLocks.SetInt(lowerCaseName, int(newStatus)); lowerCaseName.FreeSelf(); return true; } lowerCaseName.FreeSelf(); 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. /// /// 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 immutableVotingName; 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; } /// 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; } /// 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). /// /// # 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 { }