Browse Source

Refactor `Commands_Feature` to use API

pull/12/head
Anton Tarasenko 2 years ago
parent
commit
41909851f5
  1. 3
      sources/BaseAPI/Global.uc
  2. 114
      sources/LevelAPI/Features/Commands/CommandAPI.uc
  3. 64
      sources/LevelAPI/Features/Commands/Commands.uc
  4. 676
      sources/LevelAPI/Features/Commands/Commands_Feature.uc

3
sources/BaseAPI/Global.uc

@ -50,6 +50,7 @@ var public UserAPI users;
var public PlayersAPI players; var public PlayersAPI players;
var public JsonAPI json; var public JsonAPI json;
var public SchedulerAPI scheduler; var public SchedulerAPI scheduler;
var public CommandAPI commands;
var public AvariceAPI avarice; var public AvariceAPI avarice;
var public AcediaEnvironment environment; var public AcediaEnvironment environment;
@ -92,6 +93,7 @@ protected function Initialize() {
players = PlayersAPI(memory.Allocate(class'PlayersAPI')); players = PlayersAPI(memory.Allocate(class'PlayersAPI'));
scheduler = SchedulerAPI(memory.Allocate(class'SchedulerAPI')); scheduler = SchedulerAPI(memory.Allocate(class'SchedulerAPI'));
avarice = AvariceAPI(memory.Allocate(class'AvariceAPI')); avarice = AvariceAPI(memory.Allocate(class'AvariceAPI'));
commands = CommandAPI(memory.Allocate(class'CommandAPI'));
environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment')); environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment'));
} }
@ -112,6 +114,7 @@ public function DropCoreAPI() {
players = none; players = none;
json = none; json = none;
scheduler = none; scheduler = none;
commands = none;
avarice = none; avarice = none;
default.myself = none; default.myself = none;
} }

114
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 <https://www.gnu.org/licenses/>.
*/
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<Command> 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<Command> 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 {
}

64
sources/LevelAPI/Features/Commands/Commands.uc

@ -1,6 +1,8 @@
/** /**
* Config object for `Commands_Feature`. * Author: dkanus
* Copyright 2021-2022 Anton Tarasenko * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -21,64 +23,38 @@ class Commands extends FeatureConfig
perobjectconfig perobjectconfig
config(AcediaSystem); config(AcediaSystem);
var public config bool useChatInput; var public config bool useChatInput;
var public config bool useMutateInput; var public config bool useMutateInput;
var public config string chatCommandPrefix; var public config string chatCommandPrefix;
var public config array<string> allowedPlayers;
protected function HashTable ToData() protected function HashTable ToData() {
{
local int i;
local HashTable data; local HashTable data;
local ArrayList playerList;
data = __().collections.EmptyHashTable(); data = __().collections.EmptyHashTable();
data.SetBool(P("useChatInput"), useChatInput, true); data.SetBool(P("useChatInput"), useChatInput, true);
data.SetBool(P("useMutateInput"), useMutateInput, true); data.SetBool(P("useMutateInput"), useMutateInput, true);
data.SetString(P("chatCommandPrefix"), chatCommandPrefix); 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; return data;
} }
protected function FromData(HashTable source) protected function FromData(HashTable source) {
{
local int i;
local ArrayList playerList;
if (source == none) { if (source == none) {
return; return;
} }
useChatInput = source.GetBool(P("useChatInput")); useChatInput = source.GetBool(P("useChatInput"));
useMutateInput = source.GetBool(P("useMutateInput")); useMutateInput = source.GetBool(P("useMutateInput"));
chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!"); 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();
} }
protected function DefaultIt() protected function DefaultIt() {
{ useChatInput = true;
useChatInput = true; useMutateInput = true;
useMutateInput = true; chatCommandPrefix = "!";
chatCommandPrefix = "!";
allowedPlayers.length = 0;
} }
defaultproperties defaultproperties {
{
configName = "AcediaSystem" configName = "AcediaSystem"
useChatInput = true useChatInput = true
useMutateInput = true useMutateInput = true
chatCommandPrefix = "!" chatCommandPrefix = "!"
} }

676
sources/LevelAPI/Features/Commands/Commands_Feature.uc

@ -1,9 +1,8 @@
/** /**
* This feature provides a mechanism to define commands that automatically * Author: dkanus
* parse their arguments into standard Acedia collection. It also allows to * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* manage them (and specify limitation on how they can be called) in a * License: GPL
* centralized manner. * Copyright 2023 Anton Tarasenko
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -22,100 +21,72 @@
*/ */
class Commands_Feature extends Feature; class Commands_Feature extends Feature;
/** //! This feature manages commands that automatically parse their arguments into standard Acedia
* # `Commands_Feature` //! collections.
* //!
* This feature provides a mechanism to define commands that automatically //! # Implementation
* parse their arguments into standard Acedia collection. It also allows to //!
* manage them (and specify limitation on how they can be called) in a //! Implementation is simple: calling a method `RegisterCommand()` adds
* centralized manner. //! command into two caches `registeredCommands` for obtaining registered
* Support command input from chat and "mutate" command. //! commands by name and `groupedCommands` for obtaining arrays of commands by
* //! their group name. These arrays are used for providing methods for fetching
* ## Usage //! arrays of commands and obtaining pre-allocated `Command` instances by their
* //! name.
* Should be enabled like any other feature. Additionally support //! Depending on settings, this feature also connects to corresponding
* `EmergencyEnable()` enabling method that bypasses regular settings to allow //! signals for catching "mutate"/chat input, then it checks user-specified name
* admins to start this feature while forcefully enabling "mutate" command //! for being an alias and picks correct command from `registeredCommands`.
* input method. //! Emergency enabling this feature sets `emergencyEnabledMutate` flag that
* Available configuration: //! enforces connecting to the "mutate" input.
*
* 1. Whether to use command input from chat and what prefix is used to // Auxiliary struct for passing name of the command to call plus, optionally, additional
* denote a command (by default "!"); // sub-command name.
* 2. Whether to use command input from "mutate" command. //
* // Normally sub-command name is parsed by the command itself, however command aliases can try to
* To add new commands into the system - get enabled instance of this // enforce one.
* feature and call its `RegisterCommand()` method to add your custom struct CommandCallPair {
* `Command` class. `RemoveCommand()` can also be used to de-register var MutableText commandName;
* a command, if you need this for some reason. // In case it is enforced by an alias
* var MutableText subCommandName;
* ## 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 // Delimiters that always separate command name from it's parameters
var private array<Text> commandDelimiters; var private array<Text> commandDelimiters;
// Registered commands, recorded as (<command_name>, <command_instance>) pairs. // Registered commands, recorded as (<command_name>, <command_instance>) pairs.
// Keys should be deallocated when their entry is removed. // Keys should be deallocated when their entry is removed.
var private HashTable registeredCommands; var private HashTable registeredCommands;
// `HashTable` of "<command_group_name>" <-> `ArrayList` of commands pairs // `HashTable` of "<command_group_name>" <-> `ArrayList` of commands pairs to allow quick fetch of
// to allow quick fetch of commands belonging to a single group // commands belonging to a single group
var private HashTable groupedCommands; var private HashTable groupedCommands;
// When this flag is set to true, mutate input becomes available // When this flag is set to true, mutate input becomes available despite `useMutateInput` flag to
// despite `useMutateInput` flag to allow to unlock server in case of an error // allow to unlock server in case of an error
var private bool emergencyEnabledMutate; var private bool emergencyEnabledMutate;
// Setting this to `true` enables players to input commands right in the chat // Setting this to `true` enables players to input commands right in the chat by prepending them
// by prepending them with `chatCommandPrefix`. // with `chatCommandPrefix`.
// Default is `true`. // Default is `true`.
var private /*config*/ bool useChatInput; var private /*config*/ bool useChatInput;
// Setting this to `true` enables players to input commands with "mutate" // Setting this to `true` enables players to input commands with "mutate" console command.
// console command. // Default is `true`.
// Default is `true`.
var private /*config*/ bool useMutateInput; var private /*config*/ bool useMutateInput;
// Chat messages, prepended by this prefix will be treated as commands. // Chat messages, prepended by this prefix will be treated as commands.
// Default is "!". Empty values are also treated as "!". // Default is "!". Empty values are also treated as "!".
var private /*config*/ Text chatCommandPrefix; 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<string> 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; var LoggerAPI.Definition errCommandDuplicate;
protected function OnEnabled() protected function OnEnabled() {
{ registeredCommands = _.collections.EmptyHashTable();
registeredCommands = _.collections.EmptyHashTable(); groupedCommands = _.collections.EmptyHashTable();
groupedCommands = _.collections.EmptyHashTable();
RegisterCommand(class'ACommandHelp'); RegisterCommand(class'ACommandHelp');
// Macro selector // Macro selector
commandDelimiters[0] = _.text.FromString("@"); commandDelimiters[0] = _.text.FromString("@");
// Key selector // Key selector
commandDelimiters[1] = _.text.FromString("#"); commandDelimiters[1] = _.text.FromString("#");
// Player array (possibly JSON array) // Player array (possibly JSON array)
commandDelimiters[2] = _.text.FromString("["); 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("!"); commandDelimiters[3] = _.text.FromString("!");
if (useChatInput) { if (useChatInput) {
_.chat.OnMessage(self).connect = HandleCommands; _.chat.OnMessage(self).connect = HandleCommands;
@ -131,23 +102,19 @@ protected function OnEnabled()
} }
} }
protected function OnDisabled() protected function OnDisabled() {
{
if (useChatInput) { if (useChatInput) {
_.chat.OnMessage(self).Disconnect(); _.chat.OnMessage(self).Disconnect();
} }
if (useMutateInput) { if (useMutateInput) {
_server.unreal.mutator.OnMutate(self).Disconnect(); _server.unreal.mutator.OnMutate(self).Disconnect();
} }
useChatInput = false; useChatInput = false;
useMutateInput = false; useMutateInput = false;
_.memory.Free(registeredCommands); _.memory.Free3(registeredCommands, groupedCommands, chatCommandPrefix);
_.memory.Free(groupedCommands); registeredCommands = none;
_.memory.Free(chatCommandPrefix); groupedCommands = none;
_.memory.FreeMany(commandDelimiters); chatCommandPrefix = none;
registeredCommands = none;
groupedCommands = none;
chatCommandPrefix = none;
commandDelimiters.length = 0; commandDelimiters.length = 0;
} }
@ -161,50 +128,131 @@ protected function SwapConfig(FeatureConfig config)
} }
_.memory.Free(chatCommandPrefix); _.memory.Free(chatCommandPrefix);
chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix); chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix);
allowedPlayers = newConfig.allowedPlayers;
useChatInput = newConfig.useChatInput; useChatInput = newConfig.useChatInput;
useMutateInput = newConfig.useMutateInput; useMutateInput = newConfig.useMutateInput;
} }
/** // Parses command's name into `CommandCallPair` - sub-command is filled in case
* `Command_Feature` is a critical command to have running on your server and, // specified name is an alias with specified sub-command name.
* if disabled by accident, there will be no way of starting it again without private final function CommandCallPair ParseCommandCallPairWith(Parser parser) {
* restarting the level or even editing configs. local Text resolvedValue;
* local MutableText userSpecifiedName;
* This method allows to enable it along with "mutate" input in case something local CommandCallPair result;
* goes wrong. local Text.Character dotCharacter;
*/
public final static function EmergencyEnable() if (parser == none) return result;
{ if (!parser.Ok()) return result;
local Text autoConfig;
local Commands_Feature feature; 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<Command> 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(); autoConfig = GetAutoEnabledConfig();
EnableMe(autoConfig); EnableMe(autoConfig);
__().memory.Free(autoConfig); __().memory.Free(autoConfig);
} }
feature = Commands_Feature(GetEnabledInstance()); feature = Commands_Feature(GetEnabledInstance());
if ( !feature.emergencyEnabledMutate noWayToInputCommands = !feature.emergencyEnabledMutate
&& !feature.IsUsingMutateInput() && !feature.IsUsingChatInput()) &&!feature.IsUsingMutateInput()
{ && !feature.IsUsingChatInput();
if (noWayToInputCommands) {
default.emergencyEnabledMutate = true; default.emergencyEnabledMutate = true;
feature.emergencyEnabledMutate = true; feature.emergencyEnabledMutate = true;
__server().unreal.mutator.OnMutate(feature).connect = HandleMutate; __server().unreal.mutator.OnMutate(feature).connect = HandleMutate;
} }
} }
/** /// Checks if `Commands_Feature` currently uses chat as input.
* Checks if `Commands_Feature` currently uses chat as input. ///
* If `Commands_Feature` is not enabled, then it does not use anything /// If `Commands_Feature` is not enabled, then it does not use anything
* as input. /// as input.
* public final static function bool IsUsingChatInput() {
* @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; local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance()); instance = Commands_Feature(GetEnabledInstance());
@ -214,16 +262,11 @@ public final static function bool IsUsingChatInput()
return false; return false;
} }
/** /// Checks if `Commands_Feature` currently uses mutate command as input.
* Checks if `Commands_Feature` currently uses mutate command as input. ///
* If `Commands_Feature` is not enabled, then it does not use anything /// If `Commands_Feature` is not enabled, then it does not use anything
* as input. /// as input.
* public final static function bool IsUsingMutateInput() {
* @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; local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance()); instance = Commands_Feature(GetEnabledInstance());
@ -233,15 +276,10 @@ public final static function bool IsUsingMutateInput()
return false; return false;
} }
/** /// Returns prefix that will indicate that chat message is intended to be a command. By default "!".
* Returns prefix that will indicate that chat message is intended to be ///
* a command. By default "!". /// If `Commands_Feature` is disabled, always returns `none`.
* public final static function Text GetChatPrefix() {
* @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; local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance()); instance = Commands_Feature(GetEnabledInstance());
@ -251,31 +289,29 @@ public final static function Text GetChatPrefix()
return none; return none;
} }
/** /// Registers given command class, making it available.
* Registers given command class, making it available for usage. ///
* /// # Errors
* If `commandClass` provides command with a name that is already taken ///
* (comparison is case-insensitive) by a different command - a warning will be /// Returns `true` if command was successfully registered and `false` otherwise`.
* logged and newly passed `commandClass` discarded. ///
* /// If `commandClass` provides command with a name that is already taken
* @param commandClass New command class to register. /// (comparison is case-insensitive) by a different command - a warning will be
*/ /// logged and newly passed `commandClass` discarded.
public final function RegisterCommand(class<Command> commandClass) public final function bool RegisterCommand(class<Command> commandClass) {
{ local Text commandName, groupName;
local Text commandName, groupName;
local ArrayList groupArray; local ArrayList groupArray;
local Command newCommandInstance, existingCommandInstance; local Command newCommandInstance, existingCommandInstance;
if (commandClass == none) return; if (commandClass == none) return false;
if (registeredCommands == none) return; if (registeredCommands == none) return false;
newCommandInstance = Command(_.memory.Allocate(commandClass, true)); newCommandInstance = Command(_.memory.Allocate(commandClass, true));
commandName = newCommandInstance.GetName(); commandName = newCommandInstance.GetName();
groupName = newCommandInstance.GetGroupName(); groupName = newCommandInstance.GetGroupName();
// Check for duplicates and report them // Check for duplicates and report them
existingCommandInstance = Command(registeredCommands.GetItem(commandName)); existingCommandInstance = Command(registeredCommands.GetItem(commandName));
if (existingCommandInstance != none) if (existingCommandInstance != none) {
{
_.logger.Auto(errCommandDuplicate) _.logger.Auto(errCommandDuplicate)
.ArgClass(existingCommandInstance.class) .ArgClass(existingCommandInstance.class)
.Arg(commandName) .Arg(commandName)
@ -283,7 +319,7 @@ public final function RegisterCommand(class<Command> commandClass)
_.memory.Free(groupName); _.memory.Free(groupName);
_.memory.Free(newCommandInstance); _.memory.Free(newCommandInstance);
_.memory.Free(existingCommandInstance); _.memory.Free(existingCommandInstance);
return; return false;
} }
// Otherwise record new command // Otherwise record new command
// `commandName` used as a key, do not deallocate it // `commandName` used as a key, do not deallocate it
@ -295,43 +331,31 @@ public final function RegisterCommand(class<Command> commandClass)
} }
groupArray.AddItem(newCommandInstance); groupArray.AddItem(newCommandInstance);
groupedCommands.SetItem(groupName, groupArray); groupedCommands.SetItem(groupName, groupArray);
_.memory.Free(groupArray); _.memory.Free4(groupArray, groupName, commandName, newCommandInstance);
_.memory.Free(groupName); return true;
_.memory.Free(commandName);
_.memory.Free(newCommandInstance);
} }
/** /// Removes command of given class from the list of registered commands.
* Removes command of class `commandClass` 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.
* WARNING: removing once registered commands is not an action that is expected /// It is linear on the current amount of commands.
* to be performed under normal circumstances and it is not efficient. public final function RemoveCommand(class<Command> commandClass) {
* It is linear on the current amount of commands. local int i;
* local CollectionIterator iter;
* @param commandClass Class of command to remove from being registered. local Command nextCommand;
*/ local Text nextCommandName;
public final function RemoveCommand(class<Command> commandClass) local array<Text> commandGroup;
{ local array<Text> keysToRemove;
local int i;
local CollectionIterator iter;
local Command nextCommand;
local Text nextCommandName;
local array<Text> commandGroup;
local array<Text> keysToRemove;
if (commandClass == none) return; if (commandClass == none) return;
if (registeredCommands == 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()); nextCommand = Command(iter.Get());
nextCommandName = Text(iter.GetKey()); nextCommandName = Text(iter.GetKey());
if ( nextCommand == none || nextCommandName == none if (nextCommand == none || nextCommandName == none || nextCommand.class != commandClass) {
|| nextCommand.class != commandClass) _.memory.Free2(nextCommand, nextCommandName);
{
_.memory.Free(nextCommand);
_.memory.Free(nextCommandName);
continue; continue;
} }
keysToRemove[keysToRemove.length] = nextCommandName; keysToRemove[keysToRemove.length] = nextCommandName;
@ -339,57 +363,22 @@ public final function RemoveCommand(class<Command> commandClass)
_.memory.Free(nextCommand); _.memory.Free(nextCommand);
} }
iter.FreeSelf(); iter.FreeSelf();
for (i = 0; i < keysToRemove.length; i += 1) for (i = 0; i < keysToRemove.length; i += 1) {
{
registeredCommands.RemoveItem(keysToRemove[i]); registeredCommands.RemoveItem(keysToRemove[i]);
_.memory.Free(keysToRemove[i]); _.memory.Free(keysToRemove[i]);
} }
for (i = 0; i < commandGroup.length; i += 1) { for (i = 0; i < commandGroup.length; i += 1) {
RemoveClassFromGroup(commandClass, commandGroup[i]); RemoveClassFromGroup(commandClass, commandGroup[i]);
} }
_.memory.FreeMany(commandGroup); _.memory.FreeMany(commandGroup);
} }
private final function RemoveClassFromGroup( /// Returns command based on a given name.
class<Command> commandClass, ///
BaseText commandGroup) /// Name of the registered `Command` to return is case-insensitive.
{ ///
local int i; /// If no command with such name was registered - returns `none`.
local ArrayList groupArray; public final function Command GetCommand(BaseText commandName) {
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 Text commandNameLowerCase;
local Command commandInstance; local Command commandInstance;
@ -402,13 +391,8 @@ public final function Command GetCommand(BaseText commandName)
return commandInstance; return commandInstance;
} }
/** /// Returns array of names of all available commands.
* Returns array of names of all available commands. public final function array<Text> GetCommandNames() {
*
* @return Array of names of all available (registered) commands.
*/
public final function array<Text> GetCommandNames()
{
local array<Text> emptyResult; local array<Text> emptyResult;
if (registeredCommands != none) { if (registeredCommands != none) {
@ -417,26 +401,18 @@ public final function array<Text> GetCommandNames()
return emptyResult; return emptyResult;
} }
/** /// Returns array of names of all available commands belonging to the group [`groupName`].
* Returns array of names of all available commands belonging to the group public final function array<Text> GetCommandNamesInGroup(BaseText groupName) {
* `groupName`. local int i;
* local ArrayList groupArray;
* @return Array of names of all available (registered) commands, belonging to local Command nextCommand;
* the group `groupName`. local array<Text> result;
*/
public final function array<Text> GetCommandNamesInGroup(BaseText groupName)
{
local int i;
local ArrayList groupArray;
local Command nextCommand;
local array<Text> result;
if (groupedCommands == none) return result; if (groupedCommands == none) return result;
groupArray = groupedCommands.GetArrayList(groupName); groupArray = groupedCommands.GetArrayList(groupName);
if (groupArray == none) return result; 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)); nextCommand = Command(groupArray.GetItem(i));
if (nextCommand != none) { if (nextCommand != none) {
result[result.length] = nextCommand.GetName(); result[result.length] = nextCommand.GetName();
@ -446,13 +422,8 @@ public final function array<Text> GetCommandNamesInGroup(BaseText groupName)
return result; return result;
} }
/** /// Returns all available command groups' names.
* Returns all available command groups' names. public final function array<Text> GetGroupsNames() {
*
* @return Array of all available command groups' names.
*/
public final function array<Text> GetGroupsNames()
{
local array<Text> emptyResult; local array<Text> emptyResult;
if (groupedCommands != none) { if (groupedCommands != none) {
@ -461,158 +432,75 @@ public final function array<Text> GetGroupsNames()
return emptyResult; return emptyResult;
} }
/** /// Executes command based on the input.
* Handles user input: finds appropriate command and passes the rest of ///
* the arguments to it for further processing. /// Takes [`commandLine`] as input with command's call, finds appropriate registered command
* /// instance and executes it with parameters specified in the [`commandLine`].
* @param input Test that contains user's command input. ///
* @param callerPlayer Player that caused this command call. /// [`callerPlayer`] has to be specified and represents instigator of this command that will receive
*/ /// appropriate result/error messages.
public final function HandleInput(BaseText input, EPlayer callerPlayer) ///
{ /// 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; local Parser wrapper;
if (input == none) { if (input == none) {
return; return false;
} }
wrapper = input.Parse(); wrapper = input.Parse();
HandleInputWith(wrapper, callerPlayer); result = HandleInputWith(wrapper, callerPlayer);
wrapper.FreeSelf(); wrapper.FreeSelf();
return result;
} }
/** /// Executes command based on the input.
* Handles user input: finds appropriate command and passes the rest of ///
* the arguments to it for further processing. /// Takes [`commandLine`] as input with command's call, finds appropriate registered command
* /// instance and executes it with parameters specified in the [`commandLine`].
* @param parser Parser filled with user input that is expected to ///
* contain command's name and it's parameters. /// [`callerPlayer`] has to be specified and represents instigator of this command that will receive
* @param callerPlayer Player that caused this command call. /// appropriate result/error messages.
*/ ///
public final function HandleInputWith(Parser parser, EPlayer callerPlayer) /// Returns `true` iff command was successfully executed.
{ ///
local int i; /// # Errors
local bool foundID; ///
local string steamID; /// Doesn't log any errors, but can complain about errors in name or parameters to
local PlayerController controller; /// the [`callerPlayer`]
local Command commandInstance; public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) {
local Command.CallData callData; local bool errorOccured;
local CommandCallPair callPair; local Command commandInstance;
local Command.CallData callData;
if (parser == none) return; local CommandCallPair callPair;
if (callerPlayer == none) return;
if (!parser.Ok()) return; if (parser == none) return false;
controller = callerPlayer.GetController(); if (callerPlayer == none) return false;
if (controller == none) return; if (!parser.Ok()) return false;
steamID = controller.GetPlayerIDHash();
for (i = 0; i < allowedPlayers.length; i += 1)
{
if (allowedPlayers[i] == steamID)
{
foundID = true;
break;
}
}
if (!foundID) {
return;
}
callPair = ParseCommandCallPairWith(parser); callPair = ParseCommandCallPairWith(parser);
commandInstance = GetCommand(callPair.commandName); commandInstance = GetCommand(callPair.commandName);
if ( commandInstance == none if (commandInstance == none && callerPlayer != none && callerPlayer.IsExistent()) {
&& callerPlayer != none && callerPlayer.IsExistent())
{
callerPlayer callerPlayer
.BorrowConsole() .BorrowConsole()
.Flush() .Flush()
.Say(F("{$TextFailure Command not found!}")); .Say(F("{$TextFailure Command not found!}"));
} }
if (parser.Ok() && commandInstance != none) if (parser.Ok() && commandInstance != none) {
{ callData = commandInstance.ParseInputWith(parser, callerPlayer, callPair.subCommandName);
callData = commandInstance errorOccured = commandInstance.Execute(callData, callerPlayer);
.ParseInputWith(parser, callerPlayer, callPair.subCommandName);
commandInstance.Execute(callData, callerPlayer);
commandInstance.DeallocateCallData(callData); commandInstance.DeallocateCallData(callData);
} }
_.memory.Free(callPair.commandName); _.memory.Free2(callPair.commandName, callPair.subCommandName);
_.memory.Free(callPair.subCommandName); return errorOccured;
} }
// Parses command's name into `CommandCallPair` - sub-command is filled in case defaultproperties {
// 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' 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.") errCommandDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Command `%3` with the same name will be ignored.")
} }
Loading…
Cancel
Save