From 41909851f572a0385eb5d3c2264685f31fd2b120 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 13 Mar 2023 18:29:42 +0700 Subject: [PATCH] Refactor `Commands_Feature` to use API --- sources/BaseAPI/Global.uc | 3 + .../LevelAPI/Features/Commands/CommandAPI.uc | 114 +++ .../LevelAPI/Features/Commands/Commands.uc | 64 +- .../Features/Commands/Commands_Feature.uc | 676 ++++++++---------- 4 files changed, 419 insertions(+), 438 deletions(-) create mode 100644 sources/LevelAPI/Features/Commands/CommandAPI.uc diff --git a/sources/BaseAPI/Global.uc b/sources/BaseAPI/Global.uc index e87cb2f..f53553f 100644 --- a/sources/BaseAPI/Global.uc +++ b/sources/BaseAPI/Global.uc @@ -50,6 +50,7 @@ var public UserAPI users; var public PlayersAPI players; var public JsonAPI json; var public SchedulerAPI scheduler; +var public CommandAPI commands; var public AvariceAPI avarice; var public AcediaEnvironment environment; @@ -92,6 +93,7 @@ protected function Initialize() { players = PlayersAPI(memory.Allocate(class'PlayersAPI')); scheduler = SchedulerAPI(memory.Allocate(class'SchedulerAPI')); avarice = AvariceAPI(memory.Allocate(class'AvariceAPI')); + commands = CommandAPI(memory.Allocate(class'CommandAPI')); environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment')); } @@ -112,6 +114,7 @@ public function DropCoreAPI() { players = none; json = none; scheduler = none; + commands = none; avarice = none; default.myself = none; } diff --git a/sources/LevelAPI/Features/Commands/CommandAPI.uc b/sources/LevelAPI/Features/Commands/CommandAPI.uc new file mode 100644 index 0000000..8b46965 --- /dev/null +++ b/sources/LevelAPI/Features/Commands/CommandAPI.uc @@ -0,0 +1,114 @@ +/** + * 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; + +var private Commands_Feature commandsFeature; + +// DO NOT CALL MANUALLY +public final /*internal*/ function _reloadFeature() +{ + if (commandsFeature != none) { + commandsFeature.FreeSelf(); + commandsFeature = none; + } + 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, making it available via `Execute()`. +/// +/// Returns `true` if command was successfully registered and `false` otherwise`. +/// +/// # 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); + } + return false; +} + +/// 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); + } +} + +/// 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`]. +/// +/// [`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`] +public final function Execute(BaseText commandLine, EPlayer callerPlayer) { + if (commandsFeature != none) { + commandsFeature.HandleInput(commandLine, 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`]. +/// +/// [`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`] +public final function Execute_S(string commandLine, EPlayer callerPlayer) { + local MutableText wrapper; + + if (commandsFeature != none) { + wrapper = _.text.FromStringM(commandLine); + commandsFeature.HandleInput(wrapper, callerPlayer); + _.memory.Free(wrapper); + } +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/LevelAPI/Features/Commands/Commands.uc b/sources/LevelAPI/Features/Commands/Commands.uc index 35138ca..82f5ba7 100644 --- a/sources/LevelAPI/Features/Commands/Commands.uc +++ b/sources/LevelAPI/Features/Commands/Commands.uc @@ -1,6 +1,8 @@ /** - * Config object for `Commands_Feature`. - * Copyright 2021-2022 Anton Tarasenko + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -21,64 +23,38 @@ class Commands extends FeatureConfig perobjectconfig config(AcediaSystem); -var public config bool useChatInput; -var public config bool useMutateInput; -var public config string chatCommandPrefix; -var public config array allowedPlayers; +var public config bool useChatInput; +var public config bool useMutateInput; +var public config string chatCommandPrefix; -protected function HashTable ToData() -{ - local int i; +protected function HashTable ToData() { local HashTable data; - local ArrayList playerList; data = __().collections.EmptyHashTable(); data.SetBool(P("useChatInput"), useChatInput, true); data.SetBool(P("useMutateInput"), useMutateInput, true); data.SetString(P("chatCommandPrefix"), chatCommandPrefix); - playerList = _.collections.EmptyArrayList(); - for (i = 0; i < allowedPlayers.length; i += 1) { - playerList.AddString(allowedPlayers[i]); - } - data.SetItem(P("allowedPlayers"), playerList); - playerList.FreeSelf(); return data; } -protected function FromData(HashTable source) -{ - local int i; - local ArrayList playerList; - +protected function FromData(HashTable source) { if (source == none) { return; } - useChatInput = source.GetBool(P("useChatInput")); - useMutateInput = source.GetBool(P("useMutateInput")); - chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!"); - playerList = source.GetArrayList(P("allowedPlayers")); - allowedPlayers.length = 0; - if (playerList == none) { - return; - } - for (i = 0; i < playerList.GetLength(); i += 1) { - allowedPlayers[allowedPlayers.length] = playerList.GetString(i); - } - playerList.FreeSelf(); + useChatInput = source.GetBool(P("useChatInput")); + useMutateInput = source.GetBool(P("useMutateInput")); + chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!"); } -protected function DefaultIt() -{ - useChatInput = true; - useMutateInput = true; - chatCommandPrefix = "!"; - allowedPlayers.length = 0; +protected function DefaultIt() { + useChatInput = true; + useMutateInput = true; + chatCommandPrefix = "!"; } -defaultproperties -{ +defaultproperties { configName = "AcediaSystem" - useChatInput = true - useMutateInput = true - chatCommandPrefix = "!" + useChatInput = true + useMutateInput = true + chatCommandPrefix = "!" } \ No newline at end of file diff --git a/sources/LevelAPI/Features/Commands/Commands_Feature.uc b/sources/LevelAPI/Features/Commands/Commands_Feature.uc index 3b41566..30c0ec3 100644 --- a/sources/LevelAPI/Features/Commands/Commands_Feature.uc +++ b/sources/LevelAPI/Features/Commands/Commands_Feature.uc @@ -1,9 +1,8 @@ /** - * This feature provides a mechanism to define commands that automatically - * parse their arguments into standard Acedia collection. It also allows to - * manage them (and specify limitation on how they can be called) in a - * centralized manner. - * Copyright 2021-2023 Anton Tarasenko + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -22,100 +21,72 @@ */ class Commands_Feature extends Feature; -/** - * # `Commands_Feature` - * - * This feature provides a mechanism to define commands that automatically - * parse their arguments into standard Acedia collection. It also allows to - * manage them (and specify limitation on how they can be called) in a - * centralized manner. - * Support command input from chat and "mutate" command. - * - * ## Usage - * - * Should be enabled like any other feature. Additionally support - * `EmergencyEnable()` enabling method that bypasses regular settings to allow - * admins to start this feature while forcefully enabling "mutate" command - * input method. - * Available configuration: - * - * 1. Whether to use command input from chat and what prefix is used to - * denote a command (by default "!"); - * 2. Whether to use command input from "mutate" command. - * - * To add new commands into the system - get enabled instance of this - * feature and call its `RegisterCommand()` method to add your custom - * `Command` class. `RemoveCommand()` can also be used to de-register - * a command, if you need this for some reason. - * - * ## Implementation - * - * Implementation is simple: calling a method `RegisterCommand()` adds - * command into two caches `registeredCommands` for obtaining registered - * commands by name and `groupedCommands` for obtaining arrays of commands by - * their group name. These arrays are used for providing methods for fetching - * arrays of commands and obtaining pre-allocated `Command` instances by their - * name. - * Depending on settings, this feature also connects to corresponding - * signals for catching "mutate"/chat input, then it checks user-specified name - * for being an alias and picks correct command from `registeredCommands`. - * Emergency enabling this feature sets `emergencyEnabledMutate` flag that - * enforces connecting to the "mutate" input. - */ +//! This feature manages commands that automatically parse their arguments into standard Acedia +//! collections. +//! +//! # Implementation +//! +//! Implementation is simple: calling a method `RegisterCommand()` adds +//! command into two caches `registeredCommands` for obtaining registered +//! commands by name and `groupedCommands` for obtaining arrays of commands by +//! their group name. These arrays are used for providing methods for fetching +//! arrays of commands and obtaining pre-allocated `Command` instances by their +//! name. +//! Depending on settings, this feature also connects to corresponding +//! signals for catching "mutate"/chat input, then it checks user-specified name +//! for being an alias and picks correct command from `registeredCommands`. +//! Emergency enabling this feature sets `emergencyEnabledMutate` flag that +//! enforces connecting to the "mutate" input. + +// Auxiliary struct for passing name of the command to call plus, optionally, additional +// sub-command name. +// +// 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 + var MutableText subCommandName; +}; -// Delimiters that always separate command name from it's parameters +// 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. +// 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 +// `HashTable` of "" <-> `ArrayList` of commands pairs to allow quick fetch of +// commands belonging to a single group var private HashTable groupedCommands; -// 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`. +// 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`. +// 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 "!". +// Chat messages, prepended by this prefix will be treated as commands. +// Default is "!". Empty values are also treated as "!". var private /*config*/ Text chatCommandPrefix; -// List of steam IDs of players allowed to use commands. -// Temporary measure until a better solution is finished. -var private /*config*/ array allowedPlayers; - -// Contains name of the command to call plus, optionally, -// additional sub-command name. -// 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 - var MutableText subCommandName; -}; var LoggerAPI.Definition errCommandDuplicate; -protected function OnEnabled() -{ - registeredCommands = _.collections.EmptyHashTable(); - groupedCommands = _.collections.EmptyHashTable(); +protected function OnEnabled() { + registeredCommands = _.collections.EmptyHashTable(); + groupedCommands = _.collections.EmptyHashTable(); RegisterCommand(class'ACommandHelp'); - // Macro selector + // Macro selector commandDelimiters[0] = _.text.FromString("@"); - // Key selector + // Key selector commandDelimiters[1] = _.text.FromString("#"); - // Player array (possibly JSON array) + // Player array (possibly JSON array) commandDelimiters[2] = _.text.FromString("["); - // Negation of the selector + // Negation of the selector + // NOT the same thing as default command prefix in chat commandDelimiters[3] = _.text.FromString("!"); if (useChatInput) { _.chat.OnMessage(self).connect = HandleCommands; @@ -131,23 +102,19 @@ protected function OnEnabled() } } -protected function OnDisabled() -{ +protected function OnDisabled() { if (useChatInput) { _.chat.OnMessage(self).Disconnect(); } if (useMutateInput) { _server.unreal.mutator.OnMutate(self).Disconnect(); } - useChatInput = false; - useMutateInput = false; - _.memory.Free(registeredCommands); - _.memory.Free(groupedCommands); - _.memory.Free(chatCommandPrefix); - _.memory.FreeMany(commandDelimiters); - registeredCommands = none; - groupedCommands = none; - chatCommandPrefix = none; + useChatInput = false; + useMutateInput = false; + _.memory.Free3(registeredCommands, groupedCommands, chatCommandPrefix); + registeredCommands = none; + groupedCommands = none; + chatCommandPrefix = none; commandDelimiters.length = 0; } @@ -161,50 +128,131 @@ protected function SwapConfig(FeatureConfig config) } _.memory.Free(chatCommandPrefix); chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix); - allowedPlayers = newConfig.allowedPlayers; useChatInput = newConfig.useChatInput; useMutateInput = newConfig.useMutateInput; } -/** - * `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 - * restarting the level or even editing configs. - * - * This method allows to enable it along with "mutate" input in case something - * goes wrong. - */ -public final static function EmergencyEnable() -{ - local Text autoConfig; - local Commands_Feature feature; +// Parses command's name into `CommandCallPair` - sub-command is filled in case +// specified name is an alias with specified sub-command name. +private final function CommandCallPair ParseCommandCallPairWith(Parser parser) { + local Text resolvedValue; + local MutableText userSpecifiedName; + local CommandCallPair result; + local Text.Character dotCharacter; + + if (parser == none) return result; + if (!parser.Ok()) return result; + + parser.MUntilMany(userSpecifiedName, commandDelimiters, true, true); + resolvedValue = _.alias.ResolveCommand(userSpecifiedName); + // This isn't an alias + if (resolvedValue == none) { + result.commandName = userSpecifiedName; + return result; + } + // It is an alias - parse it + dotCharacter = _.text.GetCharacter("."); + resolvedValue.Parse() + .MUntil(result.commandName, dotCharacter) + .MatchS(".") + .MUntil(result.subCommandName, dotCharacter) + .FreeSelf(); + if (result.subCommandName.IsEmpty()) { + result.subCommandName.FreeSelf(); + result.subCommandName = none; + } + resolvedValue.FreeSelf(); + return result; +} - if (!IsEnabled()) - { +private function bool HandleCommands(EPlayer sender, MutableText message, bool teamMessage) { + local Parser parser; + + // We are only interested in messages that start with `chatCommandPrefix` + parser = _.text.Parse(message); + if (!parser.Match(chatCommandPrefix).Ok()) { + parser.FreeSelf(); + return true; + } + // Pass input to command feature + HandleInputWith(parser, sender); + parser.FreeSelf(); + return false; +} + +private function HandleMutate(string command, PlayerController sendingPlayer) { + local Parser parser; + local EPlayer sender; + + // A lot of other mutators use these commands + if (command ~= "help") return; + if (command ~= "version") return; + if (command ~= "status") return; + if (command ~= "credits") return; + + parser = _.text.ParseString(command); + sender = _.players.FromController(sendingPlayer); + HandleInputWith(parser, sender); + sender.FreeSelf(); + parser.FreeSelf(); +} + +private final function RemoveClassFromGroup(class commandClass, BaseText commandGroup) { + local int i; + local ArrayList groupArray; + local Command nextCommand; + + groupArray = groupedCommands.GetArrayList(commandGroup); + if (groupArray == none) { + return; + } + while (i < groupArray.GetLength()) { + nextCommand = Command(groupArray.GetItem(i)); + if (nextCommand != none && nextCommand.class == commandClass) { + groupArray.RemoveIndex(i); + } else { + i += 1; + } + _.memory.Free(nextCommand); + } + if (groupArray.GetLength() == 0) { + groupedCommands.RemoveItem(commandGroup); + } + _.memory.Free(groupArray); +} + +/// 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 +/// restarting the level or even editing configs. +public final static function EmergencyEnable() { + local bool noWayToInputCommands; + local Text autoConfig; + local Commands_Feature feature; + + if (!IsEnabled()) { autoConfig = GetAutoEnabledConfig(); EnableMe(autoConfig); __().memory.Free(autoConfig); } feature = Commands_Feature(GetEnabledInstance()); - if ( !feature.emergencyEnabledMutate - && !feature.IsUsingMutateInput() && !feature.IsUsingChatInput()) - { + noWayToInputCommands = !feature.emergencyEnabledMutate + &&!feature.IsUsingMutateInput() + && !feature.IsUsingChatInput(); + if (noWayToInputCommands) { default.emergencyEnabledMutate = true; feature.emergencyEnabledMutate = true; __server().unreal.mutator.OnMutate(feature).connect = HandleMutate; } } -/** - * Checks if `Commands_Feature` currently uses chat as input. - * If `Commands_Feature` is not enabled, then it does not use anything - * as input. - * - * @return `true` if `Commands_Feature` is currently enabled and is using chat - * as input and `false` otherwise. - */ -public final static function bool IsUsingChatInput() -{ +/// Checks if `Commands_Feature` currently uses chat as input. +/// +/// If `Commands_Feature` is not enabled, then it does not use anything +/// as input. +public final static function bool IsUsingChatInput() { local Commands_Feature instance; instance = Commands_Feature(GetEnabledInstance()); @@ -214,16 +262,11 @@ public final static function bool IsUsingChatInput() return false; } -/** - * Checks if `Commands_Feature` currently uses mutate command as input. - * If `Commands_Feature` is not enabled, then it does not use anything - * as input. - * - * @return `true` if `Commands_Feature` is currently enabled and is using - * mutate command as input and `false` otherwise. - */ -public final static function bool IsUsingMutateInput() -{ +/// Checks if `Commands_Feature` currently uses mutate command as input. +/// +/// If `Commands_Feature` is not enabled, then it does not use anything +/// as input. +public final static function bool IsUsingMutateInput() { local Commands_Feature instance; instance = Commands_Feature(GetEnabledInstance()); @@ -233,15 +276,10 @@ public final static function bool IsUsingMutateInput() return false; } -/** - * Returns prefix that will indicate that chat message is intended to be - * a command. By default "!". - * - * @return Prefix that indicates that chat message is intended to be a command. - * If `Commands_Feature` is disabled, always returns `false`. - */ -public final static function Text GetChatPrefix() -{ +/// 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() { local Commands_Feature instance; instance = Commands_Feature(GetEnabledInstance()); @@ -251,31 +289,29 @@ public final static function Text GetChatPrefix() return none; } -/** - * Registers given command class, making it available for usage. - * - * 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. - * - * @param commandClass New command class to register. - */ -public final function RegisterCommand(class commandClass) -{ - local Text commandName, groupName; +/// 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; + local Command newCommandInstance, existingCommandInstance; - if (commandClass == none) return; - if (registeredCommands == none) return; + if (commandClass == none) return false; + if (registeredCommands == none) return false; - newCommandInstance = Command(_.memory.Allocate(commandClass, true)); - commandName = newCommandInstance.GetName(); - groupName = newCommandInstance.GetGroupName(); + 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) - { + if (existingCommandInstance != none) { _.logger.Auto(errCommandDuplicate) .ArgClass(existingCommandInstance.class) .Arg(commandName) @@ -283,7 +319,7 @@ public final function RegisterCommand(class commandClass) _.memory.Free(groupName); _.memory.Free(newCommandInstance); _.memory.Free(existingCommandInstance); - return; + return false; } // Otherwise record new command // `commandName` used as a key, do not deallocate it @@ -295,43 +331,31 @@ public final function RegisterCommand(class commandClass) } groupArray.AddItem(newCommandInstance); groupedCommands.SetItem(groupName, groupArray); - _.memory.Free(groupArray); - _.memory.Free(groupName); - _.memory.Free(commandName); - _.memory.Free(newCommandInstance); + _.memory.Free4(groupArray, groupName, commandName, newCommandInstance); + return true; } -/** - * Removes command of class `commandClass` from the list of - * registered commands. - * - * WARNING: 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. - * - * @param commandClass Class of command to remove from being registered. - */ -public final function RemoveCommand(class commandClass) -{ - local int i; - local CollectionIterator iter; - local Command nextCommand; - local Text nextCommandName; - local array commandGroup; - local array keysToRemove; +/// 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()) - { + 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.Free(nextCommand); - _.memory.Free(nextCommandName); + if (nextCommand == none || nextCommandName == none || nextCommand.class != commandClass) { + _.memory.Free2(nextCommand, nextCommandName); continue; } keysToRemove[keysToRemove.length] = nextCommandName; @@ -339,57 +363,22 @@ public final function RemoveCommand(class commandClass) _.memory.Free(nextCommand); } iter.FreeSelf(); - for (i = 0; i < keysToRemove.length; i += 1) - { + 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); } -private final function RemoveClassFromGroup( - class commandClass, - BaseText commandGroup) -{ - local int i; - local ArrayList groupArray; - local Command nextCommand; - - groupArray = groupedCommands.GetArrayList(commandGroup); - if (groupArray == none) { - return; - } - while (i < groupArray.GetLength()) - { - nextCommand = Command(groupArray.GetItem(i)); - if (nextCommand != none && nextCommand.class == commandClass) { - groupArray.RemoveIndex(i); - } - else { - i += 1; - } - _.memory.Free(nextCommand); - } - if (groupArray.GetLength() == 0) { - groupedCommands.RemoveItem(commandGroup); - } - _.memory.Free(groupArray); -} - -/** - * Returns command based on a given name. - * - * @param commandName Name of the registered `Command` to return. - * Case-insensitive. - * @return Command, registered with a given name `commandName`. - * If no command with such name was registered - returns `none`. - */ -public final function Command GetCommand(BaseText commandName) -{ +/// Returns command based on a given name. +/// +/// 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; @@ -402,13 +391,8 @@ public final function Command GetCommand(BaseText commandName) return commandInstance; } -/** - * Returns array of names of all available commands. - * - * @return Array of names of all available (registered) commands. - */ -public final function array GetCommandNames() -{ +/// Returns array of names of all available commands. +public final function array GetCommandNames() { local array emptyResult; if (registeredCommands != none) { @@ -417,26 +401,18 @@ public final function array GetCommandNames() return emptyResult; } -/** - * Returns array of names of all available commands belonging to the group - * `groupName`. - * - * @return Array of names of all available (registered) 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; +/// 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) - { + for (i = 0; i < groupArray.GetLength(); i += 1) { nextCommand = Command(groupArray.GetItem(i)); if (nextCommand != none) { result[result.length] = nextCommand.GetName(); @@ -446,13 +422,8 @@ public final function array GetCommandNamesInGroup(BaseText groupName) return result; } -/** - * Returns all available command groups' names. - * - * @return Array of all available command groups' names. - */ -public final function array GetGroupsNames() -{ +/// Returns all available command groups' names. +public final function array GetGroupsNames() { local array emptyResult; if (groupedCommands != none) { @@ -461,158 +432,75 @@ public final function array GetGroupsNames() return emptyResult; } -/** - * Handles user input: finds appropriate command and passes the rest of - * the arguments to it for further processing. - * - * @param input Test that contains user's command input. - * @param callerPlayer Player that caused this command call. - */ -public final function 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`]. +/// +/// [`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`] +public final function bool HandleInput(BaseText input, EPlayer callerPlayer) { + local bool result; local Parser wrapper; if (input == none) { - return; + return false; } wrapper = input.Parse(); - HandleInputWith(wrapper, callerPlayer); + result = HandleInputWith(wrapper, callerPlayer); wrapper.FreeSelf(); + return result; } -/** - * Handles user input: finds appropriate command and passes the rest of - * the arguments to it for further processing. - * - * @param parser Parser filled with user input that is expected to - * contain command's name and it's parameters. - * @param callerPlayer Player that caused this command call. - */ -public final function HandleInputWith(Parser parser, EPlayer callerPlayer) -{ - local int i; - local bool foundID; - local string steamID; - local PlayerController controller; - local Command commandInstance; - local Command.CallData callData; - local CommandCallPair callPair; - - if (parser == none) return; - if (callerPlayer == none) return; - if (!parser.Ok()) return; - controller = callerPlayer.GetController(); - if (controller == none) return; - - steamID = controller.GetPlayerIDHash(); - for (i = 0; i < allowedPlayers.length; i += 1) - { - if (allowedPlayers[i] == steamID) - { - foundID = true; - break; - } - } - if (!foundID) { - return; - } +/// 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`]. +/// +/// [`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`] +public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) { + local bool errorOccured; + local Command commandInstance; + local Command.CallData callData; + local CommandCallPair callPair; + + if (parser == none) return false; + if (callerPlayer == none) return false; + if (!parser.Ok()) return false; + callPair = ParseCommandCallPairWith(parser); commandInstance = GetCommand(callPair.commandName); - if ( commandInstance == none - && callerPlayer != none && callerPlayer.IsExistent()) - { + 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); - commandInstance.Execute(callData, callerPlayer); + if (parser.Ok() && commandInstance != none) { + callData = commandInstance.ParseInputWith(parser, callerPlayer, callPair.subCommandName); + errorOccured = commandInstance.Execute(callData, callerPlayer); commandInstance.DeallocateCallData(callData); } - _.memory.Free(callPair.commandName); - _.memory.Free(callPair.subCommandName); + _.memory.Free2(callPair.commandName, callPair.subCommandName); + return errorOccured; } -// Parses command's name into `CommandCallPair` - sub-command is filled in case -// specified name is an alias with specified sub-command name. -private final function CommandCallPair ParseCommandCallPairWith(Parser parser) -{ - local Text resolvedValue; - local MutableText userSpecifiedName; - local CommandCallPair result; - local Text.Character dotCharacter; - - if (parser == none) return result; - if (!parser.Ok()) return result; - - parser.MUntilMany(userSpecifiedName, commandDelimiters, true, true); - resolvedValue = _.alias.ResolveCommand(userSpecifiedName); - // This isn't an alias - if (resolvedValue == none) - { - result.commandName = userSpecifiedName; - return result; - } - // It is an alias - parse it - dotCharacter = _.text.GetCharacter("."); - resolvedValue.Parse() - .MUntil(result.commandName, dotCharacter) - .MatchS(".") - .MUntil(result.subCommandName, dotCharacter) - .FreeSelf(); - if (result.subCommandName.IsEmpty()) - { - result.subCommandName.FreeSelf(); - result.subCommandName = none; - } - resolvedValue.FreeSelf(); - return result; -} - -private function bool HandleCommands( - EPlayer sender, - MutableText message, - bool teamMessage) -{ - local Parser parser; - - // We are only interested in messages that start with `chatCommandPrefix` - parser = _.text.Parse(message); - if (!parser.Match(chatCommandPrefix).Ok()) - { - parser.FreeSelf(); - return true; - } - // Pass input to command feature - HandleInputWith(parser, sender); - parser.FreeSelf(); - return false; -} - -private function HandleMutate(string command, PlayerController sendingPlayer) -{ - local Parser parser; - local EPlayer sender; - - // A lot of other mutators use these commands - if (command ~= "help") return; - if (command ~= "version") return; - if (command ~= "status") return; - if (command ~= "credits") return; - - parser = _.text.ParseString(command); - sender = _.players.FromController(sendingPlayer); - HandleInputWith(parser, sender); - sender.FreeSelf(); - parser.FreeSelf(); -} - -defaultproperties -{ +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.") } \ No newline at end of file