/**
* 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;
// 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 AssociativeArray registeredCommands;
// When this flag is set to true, mutate input becomes available
// despite `useMutateInput` flag to allow to unlock server in case of an error
var private bool emergencyEnabledMutate;
// Setting this to `true` enables players to input commands right in the chat
// by prepending them with `chatCommandPrefix`.
// Default is `true`.
var private /*config*/ bool useChatInput;
// Setting this to `true` enables players to input commands with "mutate"
// console command.
// Default is `true`.
var private /*config*/ bool useMutateInput;
// Chat messages, prepended by this prefix will be treated as commands.
// Default is "!". Empty values are also treated as "!".
var private /*config*/ Text chatCommandPrefix;
var LoggerAPI.Definition errCommandDuplicate;
protected function OnEnabled()
{
registeredCommands = _.collections.EmptyAssociativeArray();
RegisterCommand(class'ACommandHelp');
// Macro selector
commandDelimiters[0] = P("@");
// Key selector
commandDelimiters[1] = P("#");
// Player array (possibly JSON array)
commandDelimiters[2] = P("[");
// Negation of the selector
commandDelimiters[3] = P("!");
// `SwapConfig()` will no longer touch `_.unreal.mutator.OnMutate(self)`
// with `emergencyEnabledMutate` set to `true`, so we need to give
// access here
if (emergencyEnabledMutate) {
_.unreal.mutator.OnMutate(self).connect = HandleMutate;
}
}
protected function OnDisabled()
{
if (useChatInput) {
_.chat.OnMessage(self).Disconnect();
}
if (useMutateInput) {
_.unreal.mutator.OnMutate(self).Disconnect();
}
useChatInput = false;
useMutateInput = false;
if (registeredCommands != none)
{
registeredCommands.Empty(true);
registeredCommands.FreeSelf();
registeredCommands = none;
}
commandDelimiters.length = 0;
_.memory.Free(chatCommandPrefix);
chatCommandPrefix = none;
}
protected function SwapConfig(FeatureConfig config)
{
local Commands newConfig;
newConfig = Commands(config);
if (newConfig == none) {
return;
}
_.memory.Free(chatCommandPrefix);
chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix);
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) {
_.unreal.mutator.OnMutate(self).connect = HandleMutate;
}
else {
_.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(GetInstance());
if ( !feature.emergencyEnabledMutate
&& !feature.IsUsingMutateInput() && !feature.IsUsingChatInput())
{
default.emergencyEnabledMutate = true;
feature.emergencyEnabledMutate = true;
__().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(GetInstance());
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(GetInstance());
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(GetInstance());
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;
local Command newCommandInstance, existingCommandInstance;
if (commandClass == none) return;
if (registeredCommands == none) return;
newCommandInstance = Command(_.memory.Allocate(commandClass, true));
commandName = newCommandInstance.GetName();
// 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(newCommandInstance);
return;
}
// Otherwise record new command
// `commandName` used as a key, do not deallocate it
registeredCommands.SetItem(commandName, newCommandInstance, 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 Iter iter;
local Command nextCommand;
local Text nextCommandName;
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) continue;
if (nextCommandName == none) continue;
if (nextCommand.class != commandClass) continue;
keysToRemove[keysToRemove.length] = nextCommandName;
}
iter.FreeSelf();
for (i = 0; i < keysToRemove.length; i += 1) {
registeredCommands.RemoveItem(keysToRemove[i], true);
}
}
/**
* 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 int i;
local array keys;
local Text nextKeyAsText;
local array keysAsText;
if (registeredCommands == none) return keysAsText;
keys = registeredCommands.GetKeys();
for (i = 0; i < keys.length; i += 1)
{
nextKeyAsText = Text(keys[i]);
if (nextKeyAsText != none) {
keysAsText[keysAsText.length] = nextKeyAsText.Copy();
}
}
return keysAsText;
}
/**
* 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 HandleInput(Parser parser, EPlayer callerPlayer)
{
local Command commandInstance;
local Command.CallData callData;
local MutableText commandName;
if (parser == none) return;
if (!parser.Ok()) return;
parser.MUntilMany(commandName, commandDelimiters, true, true);
commandInstance = GetCommand(commandName);
commandName.FreeSelf();
if (parser.Ok() && commandInstance != none)
{
callData = commandInstance.ParseInputWith(parser, callerPlayer);
commandInstance.Execute(callData, callerPlayer);
commandInstance.DeallocateCallData(callData);
}
}
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
HandleInput(parser, sender);
parser.FreeSelf();
return false;
}
private function HandleMutate(string command, PlayerController sendingPlayer)
{
local Parser parser;
local EPlayer sender;
parser = _.text.ParseString(command);
sender = _.players.FromController(sendingPlayer);
HandleInput(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.")
}