805 lines
31 KiB
Ucode
805 lines
31 KiB
Ucode
/**
|
|
* Author: dkanus
|
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
|
* License: GPL
|
|
* Copyright 2021-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 Command extends AcediaObject
|
|
dependson(BaseText);
|
|
|
|
//! This class is meant to represent a command type.
|
|
//!
|
|
//! Command class provides an automated way to add a command to a server through
|
|
//! AcediaCore's features. It takes care of:
|
|
//!
|
|
//! 1. Verifying that player has passed correct (expected parameters);
|
|
//! 2. Parsing these parameters into usable values (both standard, built-in
|
|
//! types like `bool`, `int`, `float`, etc. and more advanced types such
|
|
//! as players lists and JSON values);
|
|
//! 3. Allowing you to easily specify a set of players you are targeting by
|
|
//! supporting several ways to refer to them, such as *by name*, *by id*
|
|
//! and *by selector* (@ and @self refer to caller player, @all refers
|
|
//! to all players).
|
|
//! 4. It can be registered inside AcediaCore's commands feature and be
|
|
//! automatically called through the unified system that supports *chat*
|
|
//! and *mutate* inputs (as well as allowing you to hook in any other
|
|
//! input source);
|
|
//! 5. Will also automatically provide a help page through built-in "help"
|
|
//! command;
|
|
//! 6. Subcommand support - when one command can have several distinct
|
|
//! functions, depending on how its called (e.g. "inventory add" vs
|
|
//! "inventory remove"). These subcommands have a special treatment in
|
|
//! help pages, which makes them more preferable, compared to simply
|
|
//! matching first `Text` argument;
|
|
//! 7. Add support for "options" - additional flags that can modify commands
|
|
//! behavior and behave like usual command options "--force"/"-f".
|
|
//! Their short versions can even be combined:
|
|
//! "give@ $ebr --ammo --force" can be rewritten as "give@ $ebr -af".
|
|
//! And they can have their own parameters: "give@all --list sharp".
|
|
//!
|
|
//! # Implementation
|
|
//!
|
|
//! The idea of `Command`'s implementation is simple: command is basically the
|
|
//! `Command.Data` struct that is filled via `CommandDataBuilder`.
|
|
//! Whenever command is called it uses `CommandParser` to parse user's input
|
|
//! based on its `Command.Data` and either report error (in case of failure) or
|
|
//! pass make `Executed()`/`ExecutedFor()` calls (in case of success).
|
|
//!
|
|
//! When command is called is decided by `Commands_Feature` that tracks possible
|
|
//! user inputs (and provides `HandleInput()`/`HandleInputWith()` methods for
|
|
//! adding custom command inputs). That feature basically parses first part of
|
|
//! the command: its name (not the subcommand's names) and target players
|
|
//! (using `PlayersParser`, but only if command is targeted).
|
|
//!
|
|
//! Majority of the command-related code either serves to build `Command.Data`
|
|
//! or to parse command input by using it (`CommandParser`).
|
|
|
|
/// Possible errors that can arise when parsing command parameters from user
|
|
/// input
|
|
enum ErrorType {
|
|
/// No error
|
|
CET_None,
|
|
/// Bad parser was provided to parse user input (this should not be possible)
|
|
CET_BadParser,
|
|
/// Sub-command name was not specified or was incorrect
|
|
/// (this should not be possible)
|
|
CET_NoSubCommands,
|
|
/// Specified sub-command does not exist
|
|
/// (only relevant when it is enforced for parser, e.g. by an alias)
|
|
CET_BadSubCommand,
|
|
/// Required param for command / option was not specified
|
|
CET_NoRequiredParam,
|
|
CET_NoRequiredParamForOption,
|
|
/// Unknown option key was specified
|
|
CET_UnknownOption,
|
|
/// Unknown short option key was specified
|
|
CET_UnknownShortOption,
|
|
/// Same option appeared twice in one command call
|
|
CET_RepeatedOption,
|
|
/// Part of user's input could not be interpreted as a part of
|
|
/// command's call
|
|
CET_UnusedCommandParameters,
|
|
/// In one short option specification (e.g. '-lah') several options require
|
|
/// parameters: this introduces ambiguity and is not allowed
|
|
CET_MultipleOptionsWithParams,
|
|
/// Targets are specified incorrectly (for targeted commands only)
|
|
CET_IncorrectTargetList,
|
|
// No targets are specified (for targeted commands only)
|
|
CET_EmptyTargetList
|
|
};
|
|
|
|
/// Structure that contains all the information about how `Command` was called.
|
|
struct CallData {
|
|
/// Targeted players (if applicable)
|
|
var public array<EPlayer> targetPlayers;
|
|
/// Specified sub-command and parameters/options
|
|
var public Text subCommandName;
|
|
/// Provided parameters and specified options
|
|
var public HashTable parameters;
|
|
var public HashTable options;
|
|
/// Errors that occurred during command call processing are described by
|
|
/// error type.
|
|
var public ErrorType parsingError;
|
|
/// Optional error textual name of the object (parameter, option, etc.)
|
|
/// that caused it.
|
|
var public Text errorCause;
|
|
};
|
|
|
|
/// Possible types of parameters.
|
|
enum ParameterType {
|
|
/// Parses into `BoolBox`
|
|
CPT_Boolean,
|
|
/// Parses into `IntBox`
|
|
CPT_Integer,
|
|
/// Parses into `FloatBox`
|
|
CPT_Number,
|
|
/// Parses into `Text`
|
|
CPT_Text,
|
|
/// Special parameter that consumes the rest of the input into `Text`
|
|
CPT_Remainder,
|
|
/// Parses into `HashTable`
|
|
CPT_Object,
|
|
/// Parses into `ArrayList`
|
|
CPT_Array,
|
|
/// Parses into any JSON value
|
|
CPT_JSON,
|
|
/// Parses into an array of specified players
|
|
CPT_Players
|
|
};
|
|
|
|
/// Possible forms a boolean variable can be used as.
|
|
/// Boolean parameter can define it's preferred format, which will be used for
|
|
/// help page generation.
|
|
enum PreferredBooleanFormat {
|
|
PBF_TrueFalse,
|
|
PBF_EnableDisable,
|
|
PBF_OnOff,
|
|
PBF_YesNo
|
|
};
|
|
|
|
// Defines a singular command parameter
|
|
struct Parameter {
|
|
/// Display name (for the needs of help page displaying)
|
|
var Text displayName;
|
|
/// Type of value this parameter would store
|
|
var ParameterType type;
|
|
/// Does it take only a singular value or can it contain several of them,
|
|
/// written in a list
|
|
var bool allowsList;
|
|
/// Variable name that will be used as a key to store parameter's value
|
|
var Text variableName;
|
|
/// (For `CPT_Boolean` type variables only) - preferred boolean format,
|
|
/// used in help pages
|
|
var PreferredBooleanFormat booleanFormat;
|
|
/// `CPT_Text` can be attempted to be auto-resolved as an alias from some
|
|
/// source during parsing.
|
|
/// For command to attempt that, this field must be not-`none` and contain
|
|
/// the name of the alias source (either "weapon", "color", "feature",
|
|
/// "entity" or some kind of custom alias source name).
|
|
///
|
|
/// Only relevant when given value is prefixed with "$" character.
|
|
var Text aliasSourceName;
|
|
};
|
|
|
|
/// Defines a sub-command of a this command
|
|
/// (specified as "<command> <sub_command>").
|
|
///
|
|
/// Using sub-command is not optional, but if none defined
|
|
/// (in `BuildData()`) / specified by the player - an empty (`name.IsEmpty()`)
|
|
/// one is automatically created / used.
|
|
struct SubCommand {
|
|
/// Name of the sub command. Cannot be `none`.
|
|
var Text name;
|
|
/// Human-readable description of the subcommand. Can be `none`.
|
|
var Text description;
|
|
/// List of required parameters of this [`Command`].
|
|
var array<Parameter> required;
|
|
/// List of optional parameters of this [`Command`].
|
|
var array<Parameter> optional;
|
|
};
|
|
|
|
/// Defines command's option (options are specified by "--long" or "-l").
|
|
/// Options are independent from sub-commands.
|
|
struct Option {
|
|
/// [`Option`]'s short name, i.e. a single letter "f" that can be specified
|
|
/// in, e.g. "-laf" type option listings
|
|
var BaseText.Character shortName;
|
|
/// [`Option`]'s full name, e.g. "--force".
|
|
var Text longName;
|
|
/// Human-readable description of the option. Can be `none`.
|
|
var Text description;
|
|
/// List of required parameters of this [`Command::Option`].
|
|
var array<Parameter> required;
|
|
/// List of required parameters of this [`Command::Option`].
|
|
var array<Parameter> optional;
|
|
};
|
|
|
|
/// Structure that defines what sub-commands and options command has
|
|
/// (and what parameters they take)
|
|
struct Data {
|
|
/// Command group this command belongs to
|
|
var protected Text group;
|
|
/// Short summary of what command does (recommended to
|
|
/// keep it to 80 characters)
|
|
var protected Text summary;
|
|
/// Available subcommands.
|
|
var protected array<SubCommand> subCommands;
|
|
/// Available options, common to all subcommands.
|
|
var protected array<Option> options;
|
|
/// `true` iff related [`Command`] targets players.
|
|
var protected bool requiresTarget;
|
|
};
|
|
var protected Data commandData;
|
|
|
|
/// Setting variable that defines a name that will be chosen for command by
|
|
/// default.
|
|
var protected const string preferredName;
|
|
/// Name that was used to register this command.
|
|
var protected Text usedName;
|
|
/// Settings variable that defines a class to be used for this [`Command`]'s
|
|
/// permissions config
|
|
var protected const class<CommandPermissions> permissionsConfigClass;
|
|
|
|
// We do not really ever need to create more than one instance of each class
|
|
// of `Command`, so we will simply store and reuse one created instance.
|
|
var private Command mainInstance;
|
|
|
|
/// When command is being executed we create several instances of
|
|
/// `ConsoleWriter` that can be used for command output.
|
|
/// They will also be automatically deallocated once command is executed.
|
|
///
|
|
/// DO NOT modify them or deallocate any of them manually.
|
|
///
|
|
/// This should make output more convenient and standardized.
|
|
///
|
|
/// 1. `publicConsole` - sends messages to all present players;
|
|
/// 2. `callerConsole` - sends messages to the player that called the command;
|
|
/// 3. `targetConsole` - sends messages to the player that is currently being
|
|
/// targeted (different each call of `ExecutedFor()` and `none` during
|
|
/// `Executed()` call);
|
|
/// 4. `othersConsole` - sends messaged to every player that is neither
|
|
/// "caller" or "target".
|
|
var protected ConsoleWriter publicConsole, othersConsole;
|
|
var protected ConsoleWriter callerConsole, targetConsole;
|
|
|
|
protected function Constructor() {
|
|
local CommandDataBuilder dataBuilder;
|
|
|
|
if (permissionsConfigClass != none) {
|
|
permissionsConfigClass.static.Initialize();
|
|
}
|
|
dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
|
|
// Let user fill-in the rest
|
|
BuildData(dataBuilder);
|
|
commandData = dataBuilder.BorrowData();
|
|
dataBuilder.FreeSelf();
|
|
dataBuilder = none;
|
|
}
|
|
|
|
protected function Finalizer() {
|
|
local int i;
|
|
local array<SubCommand> subCommands;
|
|
local array<Option> options;
|
|
|
|
DeallocateConsoles();
|
|
_.memory.Free(usedName);
|
|
_.memory.Free(commandData.summary);
|
|
usedName = none;
|
|
commandData.summary = none;
|
|
subCommands = commandData.subCommands;
|
|
for (i = 0; i < options.length; i += 1) {
|
|
_.memory.Free(subCommands[i].name);
|
|
_.memory.Free(subCommands[i].description);
|
|
CleanParameters(subCommands[i].required);
|
|
CleanParameters(subCommands[i].optional);
|
|
subCommands[i].required.length = 0;
|
|
subCommands[i].optional.length = 0;
|
|
}
|
|
commandData.subCommands.length = 0;
|
|
options = commandData.options;
|
|
for (i = 0; i < options.length; i += 1) {
|
|
_.memory.Free(options[i].longName);
|
|
_.memory.Free(options[i].description);
|
|
CleanParameters(options[i].required);
|
|
CleanParameters(options[i].optional);
|
|
options[i].required.length = 0;
|
|
options[i].optional.length = 0;
|
|
}
|
|
commandData.options.length = 0;
|
|
}
|
|
|
|
/// Initializes command, providing it with a specific name.
|
|
///
|
|
/// Argument cannot be `none`, otherwise initialization fails.
|
|
/// [`Command`] can only be successfully initialized once.
|
|
public final function bool Initialize(BaseText commandName) {
|
|
if (commandName == none) return false;
|
|
if (usedName != none) return false;
|
|
|
|
usedName = commandName.LowerCopy();
|
|
return true;
|
|
}
|
|
|
|
/// Overload this method to use `builder` to define parameters and options for
|
|
/// your command.
|
|
protected function BuildData(CommandDataBuilder builder){}
|
|
|
|
/// Overload this method to perform required actions when your command is
|
|
/// called.
|
|
///
|
|
/// [`arguments`] is a `struct` filled with parameters that your command has
|
|
/// been called with. Guaranteed to not be in error state.
|
|
/// [`instigator`] is a player that instigated this execution.
|
|
/// [`permissions`] is a config with permissions for this command call.
|
|
protected function Executed(
|
|
CallData arguments,
|
|
EPlayer instigator,
|
|
CommandPermissions permissions) {}
|
|
|
|
/// Overload this method to perform required actions when your command is called
|
|
/// with a given player as a target.
|
|
///
|
|
/// If several players have been specified - this method will be called once
|
|
/// for each.
|
|
///
|
|
/// If your command does not require a target - this method will not be called.
|
|
///
|
|
/// [`target`] is a player that this command must perform an action on.
|
|
/// [`arguments`] is a `struct` filled with parameters that your command has
|
|
/// been called with. Guaranteed to not be in error state.
|
|
/// [`instigator`] is a player that instigated this execution.
|
|
/// [`permissions`] is a config with permissions for this command call.
|
|
protected function ExecutedFor(
|
|
EPlayer target,
|
|
CallData arguments,
|
|
EPlayer instigator,
|
|
CommandPermissions permissions) {}
|
|
|
|
/// Returns an instance of command (of particular class) that is stored
|
|
/// "as a singleton" in command's class itself. Do not deallocate it.
|
|
public final static function Command GetInstance() {
|
|
if (default.mainInstance == none) {
|
|
default.mainInstance = Command(__().memory.Allocate(default.class));
|
|
}
|
|
return default.mainInstance;
|
|
}
|
|
|
|
/// Forces command to process (parse) player's input, producing a structure with
|
|
/// parsed data in Acedia's format instead.
|
|
///
|
|
/// Use `Execute()` for actually performing command's actions.
|
|
///
|
|
/// [`subCommandName`] can be optionally specified to use as sub-command.
|
|
/// If this argument's value is `none` - sub-command name will be parsed from
|
|
/// the `parser`'s data.
|
|
///
|
|
/// Returns `CallData` structure that contains all the information about
|
|
/// parameters specified in `parser`'s contents.
|
|
/// Returned structure contains objects that must be deallocated, which can
|
|
/// easily be done by the auxiliary `DeallocateCallData()` method.
|
|
public final function CallData ParseInputWith(
|
|
Parser parser,
|
|
EPlayer callerPlayer,
|
|
optional BaseText subCommandName
|
|
) {
|
|
local array<EPlayer> targetPlayers;
|
|
local CommandParser commandParser;
|
|
local CallData callData;
|
|
|
|
if (parser == none || !parser.Ok()) {
|
|
callData.parsingError = CET_BadParser;
|
|
return callData;
|
|
}
|
|
// Parse targets and handle errors that can arise here
|
|
if (commandData.requiresTarget) {
|
|
targetPlayers = ParseTargets(parser, callerPlayer);
|
|
if (!parser.Ok()) {
|
|
callData.parsingError = CET_IncorrectTargetList;
|
|
return callData;
|
|
}
|
|
if (targetPlayers.length <= 0) {
|
|
callData.parsingError = CET_EmptyTargetList;
|
|
return callData;
|
|
}
|
|
}
|
|
// Parse parameters themselves
|
|
commandParser = CommandParser(_.memory.Allocate(class'CommandParser'));
|
|
callData = commandParser.ParseWith(
|
|
parser,
|
|
commandData,
|
|
callerPlayer,
|
|
subCommandName);
|
|
callData.targetPlayers = targetPlayers;
|
|
commandParser.FreeSelf();
|
|
return callData;
|
|
}
|
|
|
|
/// Executes caller `Command` with data provided by `callData` if it is in
|
|
/// a correct state and reports error to `callerPlayer` if `callData` is
|
|
/// invalid.
|
|
///
|
|
/// Returns `true` if command was successfully executed and `false` otherwise.
|
|
/// Execution is considered successful if `Execute()` call was made, regardless
|
|
/// of whether `Command` can actually perform required action.
|
|
/// For example, giving a weapon to a player can fail because he does not have
|
|
/// enough space in his inventory, but it will still be considered a successful
|
|
/// execution as far as return value is concerned.
|
|
///
|
|
/// [`permissions`] argument is supposed to specify permissions with which this
|
|
/// command runs.
|
|
/// If [`permissionsConfigClass`] is `none`, it must always be `none`.
|
|
/// If [`permissionsConfigClass`] is not `none`, then [`permissions`] argument
|
|
/// being `none` should mean running with minimal priviledges.
|
|
public final function bool Execute(
|
|
CallData callData,
|
|
EPlayer callerPlayer,
|
|
CommandPermissions permissions
|
|
) {
|
|
local int i;
|
|
local array<EPlayer> targetPlayers;
|
|
|
|
if (callerPlayer == none) return false;
|
|
if (!callerPlayer.IsExistent()) return false;
|
|
|
|
// Report or execute
|
|
if (callData.parsingError != CET_None) {
|
|
ReportError(callData, callerPlayer);
|
|
return false;
|
|
}
|
|
targetPlayers = callData.targetPlayers;
|
|
publicConsole = _.console.ForAll();
|
|
callerConsole = _.console.For(callerPlayer);
|
|
callerConsole
|
|
.Write(P("Executing command `"))
|
|
.Write(usedName)
|
|
.Say(P("`"));
|
|
// `othersConsole` should also exist in time for `Executed()` call
|
|
othersConsole = _.console.ForAll().ButPlayer(callerPlayer);
|
|
Executed(callData, callerPlayer, permissions);
|
|
_.memory.Free(othersConsole);
|
|
if (commandData.requiresTarget) {
|
|
for (i = 0; i < targetPlayers.length; i += 1) {
|
|
targetConsole = _.console.For(targetPlayers[i]);
|
|
othersConsole = _.console
|
|
.ForAll()
|
|
.ButPlayer(callerPlayer)
|
|
.ButPlayer(targetPlayers[i]);
|
|
ExecutedFor(targetPlayers[i], callData, callerPlayer, permissions);
|
|
_.memory.Free(othersConsole);
|
|
_.memory.Free(targetConsole);
|
|
}
|
|
}
|
|
othersConsole = none;
|
|
targetConsole = none;
|
|
DeallocateConsoles();
|
|
return true;
|
|
}
|
|
|
|
/// Auxiliary method that cleans up all data and deallocates all objects inside provided structure.
|
|
public final static function DeallocateCallData(/* take */ CallData callData) {
|
|
__().memory.Free(callData.subCommandName);
|
|
__().memory.Free(callData.parameters);
|
|
__().memory.Free(callData.options);
|
|
__().memory.Free(callData.errorCause);
|
|
__().memory.FreeMany(callData.targetPlayers);
|
|
if (callData.targetPlayers.length > 0) {
|
|
callData.targetPlayers.length = 0;
|
|
}
|
|
}
|
|
|
|
private final function CleanParameters(array<Parameter> parameters) {
|
|
local int i;
|
|
|
|
for (i = 0; i < parameters.length; i += 1) {
|
|
_.memory.Free(parameters[i].displayName);
|
|
_.memory.Free(parameters[i].variableName);
|
|
_.memory.Free(parameters[i].aliasSourceName);
|
|
}
|
|
}
|
|
|
|
/// Returns name (in lower case) of the caller command class.
|
|
public final static function Text GetPreferredName() {
|
|
return __().text.FromString(Locs(default.preferredName));
|
|
}
|
|
|
|
/// Returns name (in lower case) of the caller command class.
|
|
public final static function string GetPreferredName_S() {
|
|
return Locs(default.preferredName);
|
|
}
|
|
|
|
/// Returns name (in lower case) of the caller command class.
|
|
public final function Text GetName() {
|
|
if (usedName == none) {
|
|
return P("").Copy();
|
|
}
|
|
return usedName.LowerCopy();
|
|
}
|
|
|
|
/// Returns name (in lower case) of the caller command class.
|
|
public final function string GetName_S() {
|
|
if (usedName == none) {
|
|
return "";
|
|
}
|
|
return _.text.IntoString(/*take*/ usedName.LowerCopy());
|
|
}
|
|
|
|
/// Returns group name (in lower case) of the caller command class.
|
|
public final function Text GetGroupName() {
|
|
if (commandData.group == none) {
|
|
return P("").Copy();
|
|
}
|
|
return commandData.group.LowerCopy();
|
|
}
|
|
|
|
/// Returns group name (in lower case) of the caller command class.
|
|
public final function string GetGroupName_S() {
|
|
if (commandData.group == none) {
|
|
return "";
|
|
}
|
|
return _.text.IntoString(/*take*/ commandData.group.LowerCopy());
|
|
}
|
|
|
|
/// Loads permissions config with a given name for the caller [`Command`] class.
|
|
///
|
|
/// Permission configs describe allowed usage of the [`Command`].
|
|
/// Basic settings are contained inside [`CommandPermissions`], but commands
|
|
/// should derive their own child classes for storing their settings.
|
|
///
|
|
/// Returns `none` if caller [`Command`] class didn't specify custom permission
|
|
/// settings class or provided name is invalid (according to
|
|
/// [`BaseText::IsValidName()`]).
|
|
/// Otherwise guaranteed to return a config reference.
|
|
public final static function CommandPermissions LoadConfig(BaseText configName) {
|
|
if (configName == none) return none;
|
|
if (default.permissionsConfigClass == none) return none;
|
|
|
|
// This creates default config if it is missing
|
|
default.permissionsConfigClass.static.NewConfig(configName);
|
|
return CommandPermissions(default.permissionsConfigClass.static
|
|
.GetConfigInstance(configName));
|
|
}
|
|
|
|
/// Loads permissions config with a given name for the caller [`Command`] class.
|
|
///
|
|
/// Permission configs describe allowed usage of the [`Command`].
|
|
/// Basic settings are contained inside [`CommandPermissions`], but commands
|
|
/// should derive their own child classes for storing their settings.
|
|
///
|
|
/// Returns `none` if caller [`Command`] class didn't specify custom permission
|
|
/// settings class or provided name is invalid (according to
|
|
/// [`BaseText::IsValidName()`]).
|
|
/// Otherwise guaranteed to return a config reference.
|
|
public final static function CommandPermissions LoadConfig_S(string configName) {
|
|
local MutableText wrapper;
|
|
local CommandPermissions result;
|
|
|
|
wrapper = __().text.FromStringM(configName);
|
|
result = LoadConfig(wrapper);
|
|
__().memory.Free(wrapper);
|
|
return result;
|
|
}
|
|
|
|
/// Returns subcommands of caller [`Command`] according to the provided
|
|
/// permissions.
|
|
///
|
|
/// If provided `none` as permissions, returns all available sub commands.
|
|
public final function array<Text> GetSubCommands(optional CommandPermissions permissions) {
|
|
local int i, j;
|
|
local bool addSubCommand;
|
|
local array<string> forbiddenCommands;
|
|
local array<Text> result;
|
|
|
|
forbiddenCommands = permissions.forbiddenSubCommands;
|
|
if (permissions != none) {
|
|
forbiddenCommands = permissions.forbiddenSubCommands;
|
|
}
|
|
for (i = 0; i < commandData.subCommands.length; i += 1) {
|
|
addSubCommand = true;
|
|
for (j = 0; j < forbiddenCommands.length; j += 1) {
|
|
if (commandData.subCommands[i].name.ToString() ~= forbiddenCommands[j]) {
|
|
addSubCommand = false;
|
|
break;
|
|
}
|
|
}
|
|
if (addSubCommand) {
|
|
result[result.length] = commandData.subCommands[i].name.LowerCopy();
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Returns sub commands of caller [`Command`] according to the provided
|
|
/// permissions.
|
|
///
|
|
/// If provided `none` as permissions, returns all available sub commands.
|
|
public final function array<string> GetSubCommands_S(optional CommandPermissions permissions) {
|
|
return _.text.IntoStrings(GetSubCommands(permissions));
|
|
}
|
|
|
|
/// Checks whether a given sub command (case insensitive) is allowed to be
|
|
/// executed with given permissions.
|
|
///
|
|
/// If `none` is passed as either argument, returns `true`.
|
|
///
|
|
/// Doesn't check for the existence of sub command, only that permissions do not
|
|
/// explicitly forbid it.
|
|
/// In case non-existing subcommand is passed as an argument, the result
|
|
/// should be considered undefined.
|
|
public final function bool IsSubCommandAllowed(
|
|
BaseText subCommand,
|
|
CommandPermissions permissions
|
|
) {
|
|
if (subCommand == none) return true;
|
|
if (permissions == none) return true;
|
|
|
|
return IsSubCommandAllowed_S(subCommand.ToString(), permissions);
|
|
}
|
|
|
|
/// Checks whether a given sub command (case insensitive) is allowed to be
|
|
/// executed with given permissions.
|
|
///
|
|
/// If `none` is passed for permissions, always returns `true`.
|
|
///
|
|
/// Doesn't check for the existence of sub command, only that permissions do not
|
|
/// explicitly forbid it.
|
|
/// In case non-existing sub command is passed as an argument, the result
|
|
/// should be considered undefined.
|
|
public final function bool IsSubCommandAllowed_S(
|
|
string subCommand,
|
|
CommandPermissions permissions
|
|
) {
|
|
local int i;
|
|
local array<string> forbiddenCommands;
|
|
|
|
if (permissions == none) {
|
|
return true;
|
|
}
|
|
forbiddenCommands = permissions.forbiddenSubCommands;
|
|
for (i = 0; i < forbiddenCommands.length; i += 1) {
|
|
if (subCommand ~= forbiddenCommands[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Returns `Command.Data` struct that describes caller `Command`.
|
|
///
|
|
/// Returned struct contains `Text` references that are used internally by
|
|
/// the `Command` and not their copies.
|
|
///
|
|
/// Generally this is undesired approach and leaves `Command` more vulnerable to
|
|
/// modification, but copying all the data inside would not only introduce
|
|
/// a largely pointless computational overhead, but also would require some
|
|
/// cumbersome logic.
|
|
/// This might change in the future, so deallocating any objects in the returned
|
|
/// `struct` would lead to undefined behavior.
|
|
public final function Data BorrowData() {
|
|
return commandData;
|
|
}
|
|
|
|
private final function DeallocateConsoles() {
|
|
if (publicConsole != none && publicConsole.IsAllocated()) {
|
|
_.memory.Free(publicConsole);
|
|
}
|
|
if (callerConsole != none && callerConsole.IsAllocated()) {
|
|
_.memory.Free(callerConsole);
|
|
}
|
|
if (targetConsole != none && targetConsole.IsAllocated()) {
|
|
_.memory.Free(targetConsole);
|
|
}
|
|
if (othersConsole != none && othersConsole.IsAllocated()) {
|
|
_.memory.Free(othersConsole);
|
|
}
|
|
publicConsole = none;
|
|
callerConsole = none;
|
|
targetConsole = none;
|
|
othersConsole = none;
|
|
}
|
|
|
|
/// Reports given error to the `callerPlayer`, appropriately picking
|
|
/// message color
|
|
private final function ReportError(CallData callData, EPlayer callerPlayer) {
|
|
local Text errorMessage;
|
|
local ConsoleWriter console;
|
|
|
|
if (callerPlayer == none) return;
|
|
if (!callerPlayer.IsExistent()) return;
|
|
|
|
// Setup console color
|
|
console = callerPlayer.BorrowConsole();
|
|
if (callData.parsingError == CET_EmptyTargetList) {
|
|
console.UseColor(_.color.textWarning);
|
|
} else {
|
|
console.UseColor(_.color.textFailure);
|
|
}
|
|
// Send message
|
|
errorMessage = PrintErrorMessage(callData);
|
|
console.Say(errorMessage);
|
|
errorMessage.FreeSelf();
|
|
// Restore console color
|
|
console.ResetColor().Flush();
|
|
}
|
|
|
|
private final function Text PrintErrorMessage(CallData callData) {
|
|
local Text result;
|
|
local MutableText builder;
|
|
|
|
builder = _.text.Empty();
|
|
switch (callData.parsingError) {
|
|
case CET_BadParser:
|
|
builder.Append(P("Internal error occurred: invalid parser"));
|
|
break;
|
|
case CET_NoSubCommands:
|
|
builder.Append(P("Ill defined command: no subcommands"));
|
|
break;
|
|
case CET_BadSubCommand:
|
|
builder
|
|
.Append(P("Ill defined sub-command: "))
|
|
.Append(callData.errorCause);
|
|
break;
|
|
case CET_NoRequiredParam:
|
|
builder
|
|
.Append(P("Missing required parameter: "))
|
|
.Append(callData.errorCause);
|
|
break;
|
|
case CET_NoRequiredParamForOption:
|
|
builder
|
|
.Append(P("Missing required parameter for option: "))
|
|
.Append(callData.errorCause);
|
|
break;
|
|
case CET_UnknownOption:
|
|
builder
|
|
.Append(P("Invalid option specified: "))
|
|
.Append(callData.errorCause);
|
|
break;
|
|
case CET_UnknownShortOption:
|
|
builder.Append(P("Invalid short option specified"));
|
|
break;
|
|
case CET_RepeatedOption:
|
|
builder
|
|
.Append(P("Option specified several times: "))
|
|
.Append(callData.errorCause);
|
|
break;
|
|
case CET_UnusedCommandParameters:
|
|
builder.Append(P("Part of command could not be parsed: "))
|
|
.Append(callData.errorCause);
|
|
break;
|
|
case CET_MultipleOptionsWithParams:
|
|
builder
|
|
.Append(P("Multiple short options in one declarations require parameters: "))
|
|
.Append(callData.errorCause);
|
|
break;
|
|
case CET_IncorrectTargetList:
|
|
builder
|
|
.Append(P("Target players are incorrectly specified."))
|
|
.Append(callData.errorCause);
|
|
break;
|
|
case CET_EmptyTargetList:
|
|
builder
|
|
.Append(P("List of target players is empty"))
|
|
.Append(callData.errorCause);
|
|
break;
|
|
default:
|
|
}
|
|
result = builder.Copy();
|
|
builder.FreeSelf();
|
|
return result;
|
|
}
|
|
|
|
// Auxiliary method for parsing list of targeted players.
|
|
// Assumes given parser is not `none` and not in a failed state.
|
|
// If parsing failed, guaranteed to return an empty array.
|
|
private final function array<EPlayer> ParseTargets(Parser parser, EPlayer callerPlayer) {
|
|
local array<EPlayer> targetPlayers;
|
|
local PlayersParser targetsParser;
|
|
|
|
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser'));
|
|
targetsParser.SetSelf(callerPlayer);
|
|
targetsParser.ParseWith(parser);
|
|
if (parser.Ok()) {
|
|
targetPlayers = targetsParser.GetPlayers();
|
|
}
|
|
targetsParser.FreeSelf();
|
|
return targetPlayers;
|
|
}
|
|
|
|
defaultproperties {
|
|
preferredName = ""
|
|
permissionsConfigClass = none
|
|
} |