From 3924355e792de2a9b34461ceba9dd68d0fb20f7e Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sun, 19 Mar 2023 17:35:55 +0700 Subject: [PATCH] Fix after merge file duplicate --- .../BaseAPI/API/Commands/Commands_Feature.uc | 1 + .../Features/Commands/Commands_Feature.uc | 627 ------------------ 2 files changed, 1 insertion(+), 627 deletions(-) delete mode 100644 sources/LevelAPI/Features/Commands/Commands_Feature.uc diff --git a/sources/BaseAPI/API/Commands/Commands_Feature.uc b/sources/BaseAPI/API/Commands/Commands_Feature.uc index f02ae16..cf80c5d 100644 --- a/sources/BaseAPI/API/Commands/Commands_Feature.uc +++ b/sources/BaseAPI/API/Commands/Commands_Feature.uc @@ -79,6 +79,7 @@ protected function OnEnabled() { registeredCommands = _.collections.EmptyHashTable(); groupedCommands = _.collections.EmptyHashTable(); RegisterCommand(class'ACommandHelp'); + RegisterCommand(class'ACommandNotify'); // Macro selector commandDelimiters[0] = _.text.FromString("@"); // Key selector diff --git a/sources/LevelAPI/Features/Commands/Commands_Feature.uc b/sources/LevelAPI/Features/Commands/Commands_Feature.uc deleted file mode 100644 index 2f9008c..0000000 --- a/sources/LevelAPI/Features/Commands/Commands_Feature.uc +++ /dev/null @@ -1,627 +0,0 @@ -/** - * 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-2022 Anton Tarasenko - *------------------------------------------------------------------------------ - * This file is part of Acedia. - * - * Acedia is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License, or - * (at your option) any later version. - * - * Acedia is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Acedia. If not, see . - */ -class 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. - */ - -// Delimiters that always separate command name from it's parameters -var private array commandDelimiters; -// Registered commands, recorded as (, ) pairs. -// Keys should be deallocated when their entry is removed. -var private HashTable registeredCommands; -// `HashTable` of "" <-> `ArrayList` of commands pairs -// to allow quick fetch of commands belonging to a single group -var private HashTable groupedCommands; - -// When this flag is set to true, mutate input becomes available -// despite `useMutateInput` flag to allow to unlock server in case of an error -var private bool emergencyEnabledMutate; - -// Setting this to `true` enables players to input commands right in the chat -// by prepending them with `chatCommandPrefix`. -// Default is `true`. -var private /*config*/ bool useChatInput; -// Setting this to `true` enables players to input commands with "mutate" -// console command. -// Default is `true`. -var private /*config*/ bool useMutateInput; -// Chat messages, prepended by this prefix will be treated as commands. -// Default is "!". Empty values are also treated as "!". -var private /*config*/ Text chatCommandPrefix; -// 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(); - RegisterCommand(class'ACommandHelp'); - RegisterCommand(class'ACommandNotify'); - // Macro selector - commandDelimiters[0] = _.text.FromString("@"); - // Key selector - commandDelimiters[1] = _.text.FromString("#"); - // Player array (possibly JSON array) - commandDelimiters[2] = _.text.FromString("["); - // Negation of the selector - commandDelimiters[3] = _.text.FromString("!"); -} - -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; - commandDelimiters.length = 0; -} - -protected function SwapConfig(FeatureConfig config) -{ - local Commands newConfig; - - newConfig = Commands(config); - if (newConfig == none) { - return; - } - _.memory.Free(chatCommandPrefix); - chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix); - allowedPlayers = newConfig.allowedPlayers; - if (useChatInput != newConfig.useChatInput) - { - useChatInput = newConfig.useChatInput; - if (newConfig.useChatInput) { - _.chat.OnMessage(self).connect = HandleCommands; - } - else { - _.chat.OnMessage(self).Disconnect(); - } - } - // Do not make any modifications here in case "mutate" was - // emergency-enabled - if (useMutateInput != newConfig.useMutateInput && !emergencyEnabledMutate) - { - useMutateInput = newConfig.useMutateInput; - if (newConfig.useMutateInput) { - _server.unreal.mutator.OnMutate(self).connect = HandleMutate; - } - else { - _server.unreal.mutator.OnMutate(self).Disconnect(); - } - } -} - -/** - * `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; - - if (!IsEnabled()) - { - autoConfig = GetAutoEnabledConfig(); - EnableMe(autoConfig); - __().memory.Free(autoConfig); - } - feature = Commands_Feature(GetEnabledInstance()); - if ( !feature.emergencyEnabledMutate - && !feature.IsUsingMutateInput() && !feature.IsUsingChatInput()) - { - 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() -{ - local Commands_Feature instance; - - instance = Commands_Feature(GetEnabledInstance()); - if (instance != none) { - return instance.useChatInput; - } - 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() -{ - local Commands_Feature instance; - - instance = Commands_Feature(GetEnabledInstance()); - if (instance != none) { - return instance.useMutateInput; - } - 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() -{ - local Commands_Feature instance; - - instance = Commands_Feature(GetEnabledInstance()); - if (instance != none && instance.chatCommandPrefix != none) { - return instance.chatCommandPrefix.Copy(); - } - 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; - local ArrayList groupArray; - local Command newCommandInstance, existingCommandInstance; - - if (commandClass == none) return; - if (registeredCommands == none) return; - - newCommandInstance = Command(_.memory.Allocate(commandClass, true)); - commandName = newCommandInstance.GetName(); - groupName = newCommandInstance.GetGroupName(); - // Check for duplicates and report them - existingCommandInstance = Command(registeredCommands.GetItem(commandName)); - if (existingCommandInstance != none) - { - _.logger.Auto(errCommandDuplicate) - .ArgClass(existingCommandInstance.class) - .Arg(commandName) - .ArgClass(commandClass); - _.memory.Free(groupName); - _.memory.Free(newCommandInstance); - _.memory.Free(existingCommandInstance); - return; - } - // Otherwise record new command - // `commandName` used as a key, do not deallocate it - registeredCommands.SetItem(commandName, newCommandInstance); - // Add to grouped collection - groupArray = groupedCommands.GetArrayList(groupName); - if (groupArray == none) { - groupArray = _.collections.EmptyArrayList(); - } - groupArray.AddItem(newCommandInstance); - groupedCommands.SetItem(groupName, groupArray); - _.memory.Free(groupArray); - _.memory.Free(groupName); - _.memory.Free(commandName); - _.memory.Free(newCommandInstance); -} - -/** - * 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; - - if (commandClass == none) return; - if (registeredCommands == none) return; - - for (iter = registeredCommands.Iterate(); !iter.HasFinished(); iter.Next()) - { - nextCommand = Command(iter.Get()); - nextCommandName = Text(iter.GetKey()); - if ( nextCommand == none || nextCommandName == none - || nextCommand.class != commandClass) - { - _.memory.Free(nextCommand); - _.memory.Free(nextCommandName); - continue; - } - keysToRemove[keysToRemove.length] = nextCommandName; - commandGroup[commandGroup.length] = nextCommand.GetGroupName(); - _.memory.Free(nextCommand); - } - iter.FreeSelf(); - for (i = 0; i < keysToRemove.length; i += 1) - { - registeredCommands.RemoveItem(keysToRemove[i]); - _.memory.Free(keysToRemove[i]); - } - - for (i = 0; i < commandGroup.length; i += 1) { - RemoveClassFromGroup(commandClass, commandGroup[i]); - } - _.memory.FreeMany(commandGroup); -} - -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) -{ - local Text commandNameLowerCase; - local Command commandInstance; - - if (commandName == none) return none; - if (registeredCommands == none) return none; - - commandNameLowerCase = commandName.LowerCopy(); - commandInstance = Command(registeredCommands.GetItem(commandNameLowerCase)); - commandNameLowerCase.FreeSelf(); - return commandInstance; -} - -/** - * Returns array of names of all available commands. - * - * @return Array of names of all available (registered) commands. - */ -public final function array GetCommandNames() -{ - local array emptyResult; - - if (registeredCommands != none) { - return registeredCommands.GetTextKeys(); - } - return emptyResult; -} - -/** - * Returns array of names of all available commands belonging to the group - * `groupName`. - * - * @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; - - if (groupedCommands == none) return result; - groupArray = groupedCommands.GetArrayList(groupName); - if (groupArray == none) return result; - - for (i = 0; i < groupArray.GetLength(); i += 1) - { - nextCommand = Command(groupArray.GetItem(i)); - if (nextCommand != none) { - result[result.length] = nextCommand.GetName(); - } - _.memory.Free(nextCommand); - } - return result; -} - -/** - * Returns all available command groups' names. - * - * @return Array of all available command groups' names. - */ -public final function array GetGroupsNames() -{ - local array emptyResult; - - if (groupedCommands != none) { - return groupedCommands.GetTextKeys(); - } - 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) -{ - local Parser wrapper; - - if (input == none) { - return; - } - wrapper = input.Parse(); - HandleInputWith(wrapper, callerPlayer); - wrapper.FreeSelf(); -} - -/** - * 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; - } - callPair = ParseCommandCallPairWith(parser); - commandInstance = GetCommand(callPair.commandName); - if ( commandInstance == none - && callerPlayer != none && callerPlayer.IsExistent()) - { - callerPlayer - .BorrowConsole() - .Flush() - .Say(F("{$TextFailure Command not found!}")); - } - if (parser.Ok() && commandInstance != none) - { - callData = commandInstance - .ParseInputWith(parser, callerPlayer, callPair.subCommandName); - commandInstance.Execute(callData, callerPlayer); - commandInstance.DeallocateCallData(callData); - } - _.memory.Free(callPair.commandName); - _.memory.Free(callPair.subCommandName); -} - -// 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 -{ - 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