Browse Source

Fix formatting in some commands-related classes

develop
Anton Tarasenko 1 year ago
parent
commit
d7ed4776b4
  1. 570
      sources/BaseAPI/API/Commands/Command.uc
  2. 755
      sources/BaseAPI/API/Commands/CommandDataBuilder.uc
  3. 350
      sources/BaseAPI/API/Commands/CommandParser.uc
  4. 255
      sources/BaseAPI/API/Commands/PlayersParser.uc

570
sources/BaseAPI/API/Commands/Command.uc

@ -26,7 +26,7 @@ class Command extends AcediaObject
//! //!
//! Command class provides an automated way to add a command to a server through //! Command class provides an automated way to add a command to a server through
//! AcediaCore's features. It takes care of: //! AcediaCore's features. It takes care of:
//! //!
//! 1. Verifying that player has passed correct (expected parameters); //! 1. Verifying that player has passed correct (expected parameters);
//! 2. Parsing these parameters into usable values (both standard, built-in //! 2. Parsing these parameters into usable values (both standard, built-in
//! types like `bool`, `int`, `float`, etc. and more advanced types such //! types like `bool`, `int`, `float`, etc. and more advanced types such
@ -54,92 +54,97 @@ class Command extends AcediaObject
//! //!
//! # Implementation //! # Implementation
//! //!
//! The idea of `Command`'s implementation is simple: command is basically the `Command.Data` struct //! The idea of `Command`'s implementation is simple: command is basically the
//! that is filled via `CommandDataBuilder`. //! `Command.Data` struct that is filled via `CommandDataBuilder`.
//! Whenever command is called it uses `CommandParser` to parse user's input based on its //! Whenever command is called it uses `CommandParser` to parse user's input
//! `Command.Data` and either report error (in case of failure) or pass make //! based on its `Command.Data` and either report error (in case of failure) or
//! `Executed()`/`ExecutedFor()` calls (in case of success). //! pass make `Executed()`/`ExecutedFor()` calls (in case of success).
//! //!
//! When command is called is decided by `Commands_Feature` that tracks possible user inputs //! When command is called is decided by `Commands_Feature` that tracks possible
//! (and provides `HandleInput()`/`HandleInputWith()` methods for adding custom command inputs). //! user inputs (and provides `HandleInput()`/`HandleInputWith()` methods for
//! That feature basically parses first part of the command: its name (not the subcommand's names) //! adding custom command inputs). That feature basically parses first part of
//! and target players (using `PlayersParser`, but only if command is targeted). //! 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 //! Majority of the command-related code either serves to build `Command.Data`
//! input by using it (`CommandParser`). //! or to parse command input by using it (`CommandParser`).
/// Possible errors that can arise when parsing command parameters from user input /// Possible errors that can arise when parsing command parameters from user
/// input
enum ErrorType { enum ErrorType {
// No error /// No error
CET_None, CET_None,
// Bad parser was provided to parse user input (this should not be possible) /// Bad parser was provided to parse user input (this should not be possible)
CET_BadParser, CET_BadParser,
// Sub-command name was not specified or was incorrect /// Sub-command name was not specified or was incorrect
// (this should not be possible) /// (this should not be possible)
CET_NoSubCommands, CET_NoSubCommands,
// Specified sub-command does not exist /// Specified sub-command does not exist
// (only relevant when it is enforced for parser, e.g. by an alias) /// (only relevant when it is enforced for parser, e.g. by an alias)
CET_BadSubCommand, CET_BadSubCommand,
// Required param for command / option was not specified /// Required param for command / option was not specified
CET_NoRequiredParam, CET_NoRequiredParam,
CET_NoRequiredParamForOption, CET_NoRequiredParamForOption,
// Unknown option key was specified /// Unknown option key was specified
CET_UnknownOption, CET_UnknownOption,
/// Unknown short option key was specified
CET_UnknownShortOption, CET_UnknownShortOption,
// Same option appeared twice in one command call /// Same option appeared twice in one command call
CET_RepeatedOption, CET_RepeatedOption,
// Part of user's input could not be interpreted as a part of /// Part of user's input could not be interpreted as a part of
// command's call /// command's call
CET_UnusedCommandParameters, CET_UnusedCommandParameters,
// In one short option specification (e.g. '-lah') several options require parameters: /// In one short option specification (e.g. '-lah') several options require
// this introduces ambiguity and is not allowed /// parameters: this introduces ambiguity and is not allowed
CET_MultipleOptionsWithParams, CET_MultipleOptionsWithParams,
// (For targeted commands only) /// Targets are specified incorrectly (for targeted commands only)
// Targets are specified incorrectly (or none actually specified)
CET_IncorrectTargetList, CET_IncorrectTargetList,
// No targets are specified (for targeted commands only)
CET_EmptyTargetList CET_EmptyTargetList
}; };
/// Structure that contains all the information about how `Command` was called. /// Structure that contains all the information about how `Command` was called.
struct CallData { struct CallData {
// Targeted players (if applicable) /// Targeted players (if applicable)
var public array<EPlayer> targetPlayers; var public array<EPlayer> targetPlayers;
// Specified sub-command and parameters/options /// Specified sub-command and parameters/options
var public Text subCommandName; var public Text subCommandName;
// Provided parameters and specified options /// Provided parameters and specified options
var public HashTable parameters; var public HashTable parameters;
var public HashTable options; var public HashTable options;
// Errors that occurred during command call processing are described by /// Errors that occurred during command call processing are described by
// error type and optional error textual name of the object /// error type.
// (parameter, option, etc.) that caused it.
var public ErrorType parsingError; var public ErrorType parsingError;
/// Optional error textual name of the object (parameter, option, etc.)
/// that caused it.
var public Text errorCause; var public Text errorCause;
}; };
/// Possible types of parameters. /// Possible types of parameters.
enum ParameterType { enum ParameterType {
// Parses into `BoolBox` /// Parses into `BoolBox`
CPT_Boolean, CPT_Boolean,
// Parses into `IntBox` /// Parses into `IntBox`
CPT_Integer, CPT_Integer,
// Parses into `FloatBox` /// Parses into `FloatBox`
CPT_Number, CPT_Number,
// Parses into `Text` /// Parses into `Text`
CPT_Text, CPT_Text,
// Special parameter that consumes the rest of the input into `Text` /// Special parameter that consumes the rest of the input into `Text`
CPT_Remainder, CPT_Remainder,
// Parses into `HashTable` /// Parses into `HashTable`
CPT_Object, CPT_Object,
// Parses into `ArrayList` /// Parses into `ArrayList`
CPT_Array, CPT_Array,
// Parses into any JSON value /// Parses into any JSON value
CPT_JSON, CPT_JSON,
// Parses into an array of specified players /// Parses into an array of specified players
CPT_Players CPT_Players
}; };
/// Possible forms a boolean variable can be used as. /// 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. /// Boolean parameter can define it's preferred format, which will be used for
/// help page generation.
enum PreferredBooleanFormat { enum PreferredBooleanFormat {
PBF_TrueFalse, PBF_TrueFalse,
PBF_EnableDisable, PBF_EnableDisable,
@ -149,91 +154,117 @@ enum PreferredBooleanFormat {
// Defines a singular command parameter // Defines a singular command parameter
struct Parameter { struct Parameter {
// Display name (for the needs of help page displaying) /// Display name (for the needs of help page displaying)
var Text displayName; var Text displayName;
// Type of value this parameter would store /// Type of value this parameter would store
var ParameterType type; var ParameterType type;
// Does it take only a singular value or can it contain several of them, /// Does it take only a singular value or can it contain several of them,
// written in a list /// written in a list
var bool allowsList; var bool allowsList;
// Variable name that will be used as a key to store parameter's value /// Variable name that will be used as a key to store parameter's value
var Text variableName; var Text variableName;
// (For `CPT_Boolean` type variables only) - preferred boolean format, /// (For `CPT_Boolean` type variables only) - preferred boolean format,
// used in help pages /// used in help pages
var PreferredBooleanFormat booleanFormat; var PreferredBooleanFormat booleanFormat;
// `CPT_Text` can be attempted to be auto-resolved as an alias from some source during parsing. /// `CPT_Text` can be attempted to be auto-resolved as an alias from some
// For command to attempt that, this field must be not-`none` and contain the name of /// source during parsing.
// the alias source (either "weapon", "color", "feature", "entity" or some kind of custom alias /// For command to attempt that, this field must be not-`none` and contain
// source name). /// 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. ///
/// Only relevant when given value is prefixed with "$" character.
var Text aliasSourceName; var Text aliasSourceName;
}; };
// Defines a sub-command of a this command (specified as "<command> <sub_command>"). /// 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. /// 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 { struct SubCommand {
// Cannot be `none` /// Name of the sub command. Cannot be `none`.
var Text name; var Text name;
// Can be `none` /// Human-readable description of the subcommand. Can be `none`.
var Text description; var Text description;
/// List of required parameters of this [`Command`].
var array<Parameter> required; var array<Parameter> required;
/// List of optional parameters of this [`Command`].
var array<Parameter> optional; var array<Parameter> optional;
}; };
// Defines command's option (options are specified by "--long" or "-l"). /// Defines command's option (options are specified by "--long" or "-l").
// Options are independent from sub-commands. /// Options are independent from sub-commands.
struct Option { 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; var BaseText.Character shortName;
/// [`Option`]'s full name, e.g. "--force".
var Text longName; var Text longName;
/// Human-readable description of the option. Can be `none`.
var Text description; var Text description;
// Option can also have their own parameters /// List of required parameters of this [`Command::Option`].
var array<Parameter> required; var array<Parameter> required;
/// List of required parameters of this [`Command::Option`].
var array<Parameter> optional; var array<Parameter> optional;
}; };
// Structure that defines what sub-commands and options command has /// Structure that defines what sub-commands and options command has
// (and what parameters they take) /// (and what parameters they take)
struct Data { struct Data {
// Default command name that will be used unless Acedia is configured to /// Command group this command belongs to
// do otherwise
var protected Text name;
// Command group this command belongs to
var protected Text group; var protected Text group;
// Short summary of what command does (recommended to /// Short summary of what command does (recommended to
// keep it to 80 characters) /// keep it to 80 characters)
var protected Text summary; var protected Text summary;
/// Available subcommands.
var protected array<SubCommand> subCommands; var protected array<SubCommand> subCommands;
/// Available options, common to all subcommands.
var protected array<Option> options; var protected array<Option> options;
/// `true` iff related [`Command`] targets players.
var protected bool requiresTarget; var protected bool requiresTarget;
}; };
var protected Data commandData; 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 // 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. // of `Command`, so we will simply store and reuse one created instance.
var private Command mainInstance; var private Command mainInstance;
/// When command is being executed we create several instances of
// When command is being executed we create several instances of `ConsoleWriter` that can be used /// `ConsoleWriter` that can be used for command output.
// for command output. They will also be automatically deallocated once command is executed. /// They will also be automatically deallocated once command is executed.
// ///
// DO NOT modify them or deallocate any of them manually. /// DO NOT modify them or deallocate any of them manually.
// ///
// This should make output more convenient and standardized. /// This should make output more convenient and standardized.
// ///
// 1. `publicConsole` - sends messages to all present players; /// 1. `publicConsole` - sends messages to all present players;
// 2. `callerConsole` - sends messages to the player that called the command; /// 2. `callerConsole` - sends messages to the player that called the command;
// 3. `targetConsole` - sends messages to the player that is currently being targeted /// 3. `targetConsole` - sends messages to the player that is currently being
// (different each call of `ExecutedFor()` and `none` during `Executed()` call); /// targeted (different each call of `ExecutedFor()` and `none` during
// 4. `othersConsole` - sends messaged to every player that is neither "caller" or "target". /// `Executed()` call);
/// 4. `othersConsole` - sends messaged to every player that is neither
/// "caller" or "target".
var protected ConsoleWriter publicConsole, othersConsole; var protected ConsoleWriter publicConsole, othersConsole;
var protected ConsoleWriter callerConsole, targetConsole; var protected ConsoleWriter callerConsole, targetConsole;
protected function Constructor() { protected function Constructor() {
local CommandDataBuilder dataBuilder; local CommandDataBuilder dataBuilder;
if (permissionsConfigClass != none) {
permissionsConfigClass.static.Initialize();
}
dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder')); dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
// Let user fill-in the rest
BuildData(dataBuilder); BuildData(dataBuilder);
commandData = dataBuilder.BorrowData(); commandData = dataBuilder.BorrowData();
dataBuilder.FreeSelf(); dataBuilder.FreeSelf();
@ -246,8 +277,10 @@ protected function Finalizer() {
local array<Option> options; local array<Option> options;
DeallocateConsoles(); DeallocateConsoles();
_.memory.Free(commandData.name); _.memory.Free(usedName);
_.memory.Free(commandData.summary); _.memory.Free(commandData.summary);
usedName = none;
commandData.summary = none;
subCommands = commandData.subCommands; subCommands = commandData.subCommands;
for (i = 0; i < options.length; i += 1) { for (i = 0; i < options.length; i += 1) {
_.memory.Free(subCommands[i].name); _.memory.Free(subCommands[i].name);
@ -270,43 +303,55 @@ protected function Finalizer() {
commandData.options.length = 0; commandData.options.length = 0;
} }
private final function CleanParameters(array<Parameter> parameters) { /// Initializes command, providing it with a specific name.
local int i; ///
/// 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;
for (i = 0; i < parameters.length; i += 1) { usedName = commandName.LowerCopy();
_.memory.Free(parameters[i].displayName); return true;
_.memory.Free(parameters[i].variableName);
_.memory.Free(parameters[i].aliasSourceName);
}
} }
/// Overload this method to use `builder` to define parameters and options for your command. /// Overload this method to use `builder` to define parameters and options for
/// your command.
protected function BuildData(CommandDataBuilder builder){} protected function BuildData(CommandDataBuilder builder){}
/// Overload this method to perform required actions when your command is called. /// 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.
/// ///
/// [`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. /// [`instigator`] is a player that instigated this execution.
protected function Executed(CallData arguments, EPlayer instigator){} /// [`permissions`] is a config with permissions for this command call.
protected function Executed(
/// Overload this method to perform required actions when your command is called with a given player CallData arguments,
/// as a target. 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 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. /// 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. /// [`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. /// [`arguments`] is a `struct` filled with parameters that your command has
/// Guaranteed to not be in error state. /// been called with. Guaranteed to not be in error state.
///
/// [`instigator`] is a player that instigated this execution. /// [`instigator`] is a player that instigated this execution.
protected function ExecutedFor(EPlayer target, CallData arguments, EPlayer instigator) {} /// [`permissions`] is a config with permissions for this command call.
protected function ExecutedFor(
/// Returns an instance of command (of particular class) that is stored "as a singleton" in EPlayer target,
/// command's class itself. Do not deallocate it. 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() { public final static function Command GetInstance() {
if (default.mainInstance == none) { if (default.mainInstance == none) {
default.mainInstance = Command(__().memory.Allocate(default.class)); default.mainInstance = Command(__().memory.Allocate(default.class));
@ -314,18 +359,19 @@ public final static function Command GetInstance() {
return default.mainInstance; return default.mainInstance;
} }
/// Forces command to process (parse) player's input, producing a structure with parsed data in /// Forces command to process (parse) player's input, producing a structure with
/// Acedia's format instead. /// parsed data in Acedia's format instead.
/// ///
/// Use `Execute()` for actually performing command's actions. /// Use `Execute()` for actually performing command's actions.
/// ///
/// [`subCommandName`] can be optionally specified to use as sub-command. /// [`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. /// 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 /// Returns `CallData` structure that contains all the information about
/// `parser`'s contents. /// parameters specified in `parser`'s contents.
/// Returned structure contains objects that must be deallocated, which can easily be done by /// Returned structure contains objects that must be deallocated, which can
/// the auxiliary `DeallocateCallData()` method. /// easily be done by the auxiliary `DeallocateCallData()` method.
public final function CallData ParseInputWith( public final function CallData ParseInputWith(
Parser parser, Parser parser,
EPlayer callerPlayer, EPlayer callerPlayer,
@ -363,16 +409,27 @@ public final function CallData ParseInputWith(
return callData; return callData;
} }
/// Executes caller `Command` with data provided by `callData` if it is in a correct state and /// Executes caller `Command` with data provided by `callData` if it is in
/// reports error to `callerPlayer` if `callData` is invalid. /// a correct state and reports error to `callerPlayer` if `callData` is
/// invalid.
/// ///
/// Returns `true` if command was successfully executed and `false` otherwise. /// Returns `true` if command was successfully executed and `false` otherwise.
/// Execution is considered successful if `Execute()` call was made, regardless of whether `Command` /// Execution is considered successful if `Execute()` call was made, regardless
/// can actually perform required action. /// 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 /// For example, giving a weapon to a player can fail because he does not have
/// inventory, but it will still be considered a successful execution as far as return value is /// enough space in his inventory, but it will still be considered a successful
/// concerned. /// execution as far as return value is concerned.
public final function bool Execute(CallData callData, EPlayer callerPlayer) { ///
/// [`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 int i;
local array<EPlayer> targetPlayers; local array<EPlayer> targetPlayers;
@ -389,11 +446,11 @@ public final function bool Execute(CallData callData, EPlayer callerPlayer) {
callerConsole = _.console.For(callerPlayer); callerConsole = _.console.For(callerPlayer);
callerConsole callerConsole
.Write(P("Executing command `")) .Write(P("Executing command `"))
.Write(commandData.name) .Write(usedName)
.Say(P("`")); .Say(P("`"));
// `othersConsole` should also exist in time for `Executed()` call // `othersConsole` should also exist in time for `Executed()` call
othersConsole = _.console.ForAll().ButPlayer(callerPlayer); othersConsole = _.console.ForAll().ButPlayer(callerPlayer);
Executed(callData, callerPlayer); Executed(callData, callerPlayer, permissions);
_.memory.Free(othersConsole); _.memory.Free(othersConsole);
if (commandData.requiresTarget) { if (commandData.requiresTarget) {
for (i = 0; i < targetPlayers.length; i += 1) { for (i = 0; i < targetPlayers.length; i += 1) {
@ -402,7 +459,7 @@ public final function bool Execute(CallData callData, EPlayer callerPlayer) {
.ForAll() .ForAll()
.ButPlayer(callerPlayer) .ButPlayer(callerPlayer)
.ButPlayer(targetPlayers[i]); .ButPlayer(targetPlayers[i]);
ExecutedFor(targetPlayers[i], callData, callerPlayer); ExecutedFor(targetPlayers[i], callData, callerPlayer, permissions);
_.memory.Free(othersConsole); _.memory.Free(othersConsole);
_.memory.Free(targetConsole); _.memory.Free(targetConsole);
} }
@ -413,6 +470,209 @@ public final function bool Execute(CallData callData, EPlayer callerPlayer) {
return true; 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() { private final function DeallocateConsoles() {
if (publicConsole != none && publicConsole.IsAllocated()) { if (publicConsole != none && publicConsole.IsAllocated()) {
_.memory.Free(publicConsole); _.memory.Free(publicConsole);
@ -432,20 +692,8 @@ private final function DeallocateConsoles() {
othersConsole = none; othersConsole = none;
} }
/// Auxiliary method that cleans up all data and deallocates all objects inside provided structure. /// Reports given error to the `callerPlayer`, appropriately picking
public final static function DeallocateCallData(/* take */ CallData callData) { /// message color
__().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;
}
}
// Reports given error to the `callerPlayer`, appropriately picking
// message color
private final function ReportError(CallData callData, EPlayer callerPlayer) { private final function ReportError(CallData callData, EPlayer callerPlayer) {
local Text errorMessage; local Text errorMessage;
local ConsoleWriter console; local ConsoleWriter console;
@ -551,35 +799,7 @@ private final function array<EPlayer> ParseTargets(Parser parser, EPlayer caller
return targetPlayers; return targetPlayers;
} }
/// Returns name (in lower case) of the caller command class.
public final function Text GetName() {
if (commandData.name == none) {
return P("").Copy();
}
return commandData.name.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 `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;
}
defaultproperties { defaultproperties {
preferredName = ""
permissionsConfigClass = none
} }

755
sources/BaseAPI/API/Commands/CommandDataBuilder.uc

File diff suppressed because it is too large Load Diff

350
sources/BaseAPI/API/Commands/CommandParser.uc

@ -23,51 +23,60 @@ class CommandParser extends AcediaObject
dependson(Command); dependson(Command);
//! Class specialized for parsing user input of the command's call into `Command.CallData` structure //! Class specialized for parsing user input of the command's call into
//! with the information about all parsed arguments. //![ `Command.CallData`] structure with the information about all parsed
//! arguments.
//! //!
//! `CommandParser` is not made to parse the whole input: //! [`CommandParser`] is not made to parse the whole input:
//! //!
//! * Command's name needs to be parsed and resolved as an alias before using this parser - //! * Command's name needs to be parsed and resolved as an alias before using
//! it won't do this hob for you; //! this parser - it won't do this hob for you;
//! * List of targeted players must also be parsed using `PlayersParser` - `CommandParser` won't do //! * List of targeted players must also be parsed using [`PlayersParser`] -
//! this for you; //! [`CommandParser`] won't do this for you;
//! * Optionally one can also decide on the referred subcommand and pass it into `ParseWith()` //! * Optionally one can also decide on the referred subcommand and pass it into
//! method. If subcommand's name is not passed - `CommandParser` will try to parse it itself. //! [`ParseWith()`] method. If subcommand's name is not passed -
//! [`CommandParser`] will try to parse it itself.
//! This feature is used to add support for subcommand aliases. //! This feature is used to add support for subcommand aliases.
//! //!
//! However, above steps are handled by `Commands_Feature` and one only needs to call that feature's //! However, above steps are handled by [`Commands_Feature`] and one only needs to
//! `HandleInput()` methods to pass user input with command call line there. //! call that feature's [`HandleInput()`] methods to pass user input with command
//! call line there.
//! //!
//! # Usage //! # Usage
//! //!
//! Allocate `CommandParser` and call `ParseWith()` method, providing it with: //! Allocate [`CommandParser`] and call [`ParseWith()`] method, providing it with:
//! //!
//! 1. `Parser`, filled with command call input; //! 1. [`Parser`], filled with command call input;
//! 2. Command's data that describes subcommands, options and their parameters for the command, //! 2. Command's data that describes subcommands, options and their parameters
//! which call we are parsing; //! for the command, which call we are parsing;
//! 3. (Optionally) `EPlayer` reference to the player that initiated the command call; //! 3. (Optionally) [`EPlayer`] reference to the player that initiated
//! 4. (Optionally) Subcommand to be used - this will prevent `CommandParser` from parsing //! the command call;
//! subcommand name itself. Used for implementing aliases that refer to a particular subcommand. //! 4. (Optionally) Subcommand to be used - this will prevent [`CommandParser`]
//! from parsing subcommand name itself. Used for implementing aliases that
//! refer to a particular subcommand.
//! //!
//! # Implementation //! # Implementation
//! //!
//! `CommandParser` stores both its state and command data, relevant to parsing, as its member //! [`CommandParser`] stores both its state and command data, relevant to parsing,
//! variables during the whole parsing process, instead of passing that data around in every single //! as its member variables during the whole parsing process, instead of passing
//! method. //! that data around in every single method.
//! //!
//! We will give a brief overview of how around 20 parsing methods below are interconnected. //! We will give a brief overview of how around 20 parsing methods below are
//! interconnected.
//! //!
//! The only public method `ParseWith()` is used to start parsing and it uses `PickSubCommand()` to //! The only public method [`ParseWith()`] is used to start parsing and it uses
//! first try and figure out what sub command is intended by user's input. //! [`PickSubCommand()`] to first try and figure out what sub command is
//! intended by user's input.
//! //!
//! Main bulk of the work is done by `ParseParameterArrays()` method, for simplicity broken into two //! Main bulk of the work is done by [`ParseParameterArrays()`] method, for
//! `ParseRequiredParameterArray()` and `ParseOptionalParameterArray()` methods that can parse //! simplicity broken into two [`ParseRequiredParameterArray()`] and
//! [`ParseOptionalParameterArray()`] methods that can parse
//! parameters for both command itself and it's options. //! parameters for both command itself and it's options.
//! //!
//! They go through arrays of required and optional parameters, calling `ParseParameter()` for each //! They go through arrays of required and optional parameters, calling
//! parameters, which in turn can make several calls of `ParseSingleValue()` to parse parameters' //! [`ParseParameter()`] for each parameters, which in turn can make several
//! values: it is called once for single-valued parameters, but possibly several times for list //! calls of [`ParseSingleValue()`] to parse parameters' values: it is called
//! once for single-valued parameters, but possibly several times for list
//! parameters that can contain several values. //! parameters that can contain several values.
//! //!
//! So main parsing method looks something like: //! So main parsing method looks something like:
@ -80,22 +89,25 @@ class CommandParser extends AcediaObject
//! } //! }
//! ``` //! ```
//! //!
//! `ParseSingleValue()` is essentially that redirects it's method call to another, more specific, //! [`ParseSingleValue()`] is essentially that redirects it's method call to
//! parsing method based on the parameter type. //! another, more specific, parsing method based on the parameter type.
//! //!
//! Finally, to allow users to specify options at any point in command, we call //! Finally, to allow users to specify options at any point in command, we call
//! `TryParsingOptions()` at the beginning of every `ParseSingleValue()` (the only parameter that //! [`TryParsingOptions()`] at the beginning of every [`ParseSingleValue()`]
//! has higher priority than options is `CPT_Remainder`), since option definition can appear at any //! (the only parameter that has higher priority than options is
//! place between parameters. We also call `TryParsingOptions()` *after* we've parsed all command's //! [`CPT_Remainder`]), since option definition can appear at any place between
//! parameters, since that case won't be detected by parsing them *before* every parameter. //! parameters. We also call `TryParsingOptions()` *after* we've parsed all
//! command's parameters, since that case won't be detected by parsing them
//! *before* every parameter.
//! //!
//! `TryParsingOptions()` itself simply tries to detect "-" and "--" prefixes (filtering out //! [`TryParsingOptions()`] itself simply tries to detect "-" and "--" prefixes
//! negative numeric values) and then redirect the call to either of more specialized methods: //! (filtering out negative numeric values) and then redirect the call to either
//! `ParseLongOption()` or `ParseShortOption()`, that can in turn make another //! of more specialized methods: [`ParseLongOption()`] or
//! `ParseParameterArrays()` call, if specified option has parameters. //! [`ParseShortOption()`], that can in turn make another
//! [`ParseParameterArrays()`] call, if specified option has parameters.
//! //!
//! NOTE: `ParseParameterArrays()` can only nest in itself once, since option declaration always //! NOTE: [`ParseParameterArrays()`] can only nest in itself once, since option
//! interrupts previous option's parameter list. //! declaration always interrupts previous option's parameter list.
//! Rest of the methods perform simple auxiliary functions. //! Rest of the methods perform simple auxiliary functions.
// Describes which parameters we are currently parsing, classifying them // Describes which parameters we are currently parsing, classifying them
@ -107,7 +119,7 @@ class CommandParser extends AcediaObject
// * Still parsing required *parameter* "integer list"; // * Still parsing required *parameter* "integer list";
// * But no more integers are *necessary* for successful parsing. // * But no more integers are *necessary* for successful parsing.
// //
// Therefore we consider parameter "necessary" if the lack of it will\ // Therefore we consider parameter "necessary" if the lack of it will
// result in failed parsing and "extra" otherwise. // result in failed parsing and "extra" otherwise.
enum ParsingTarget { enum ParsingTarget {
// We are in the process of parsing required parameters, that must all // We are in the process of parsing required parameters, that must all
@ -123,31 +135,31 @@ enum ParsingTarget {
}; };
// Parser filled with user input. // Parser filled with user input.
var private Parser commandParser; var private Parser commandParser;
// Data for sub-command specified by both command we are parsing // Data for sub-command specified by both command we are parsing
// and user's input; determined early during parsing. // and user's input; determined early during parsing.
var private Command.SubCommand pickedSubCommand; var private Command.SubCommand pickedSubCommand;
// Options available for the command we are parsing. // Options available for the command we are parsing.
var private array<Command.Option> availableOptions; var private array<Command.Option> availableOptions;
// Result variable we are filling during the parsing process, // Result variable we are filling during the parsing process,
// should be `none` outside of `self.ParseWith()` method call. // should be `none` outside of [`self.ParseWith()`] method call.
var private Command.CallData nextResult; var private Command.CallData nextResult;
// Parser for player parameters, setup with a caller for current parsing // Parser for player parameters, setup with a caller for current parsing
var private PlayersParser currentPlayersParser; var private PlayersParser currentPlayersParser;
// Current `ParsingTarget`, see it's enum description for more details // Current [`ParsingTarget`], see it's enum description for more details
var private ParsingTarget currentTarget; var private ParsingTarget currentTarget;
// `true` means we are parsing parameters for a command's option and // `true` means we are parsing parameters for a command's option and
// `false` means we are parsing command's own parameters // `false` means we are parsing command's own parameters
var private bool currentTargetIsOption; var private bool currentTargetIsOption;
// If we are parsing parameters for an option (`currentTargetIsOption == true`) // If we are parsing parameters for an option (`currentTargetIsOption == true`)
// this variable will store that option's data. // this variable will store that option's data.
var private Command.Option targetOption; var private Command.Option targetOption;
// Last successful state of `commandParser`. // Last successful state of [`commandParser`].
var Parser.ParserState confirmedState; var Parser.ParserState confirmedState;
// Options we have so far encountered during parsing, necessary since we want // Options we have so far encountered during parsing, necessary since we want
// to forbid specifying th same option more than once. // to forbid specifying th same option more than once.
var private array<Command.Option> usedOptions; var private array<Command.Option> usedOptions;
// Literals that can be used as boolean values // Literals that can be used as boolean values
var private array<string> booleanTrueEquivalents; var private array<string> booleanTrueEquivalents;
@ -159,6 +171,61 @@ protected function Finalizer() {
Reset(); Reset();
} }
/// Parses user's input given in [`parser`] using command's information given by
/// [`commandData`].
///
/// Optionally, sub-command can be specified for the [`CommandParser`] to use
/// via [`specifiedSubCommand`] argument.
/// If this argument's value is `none` - it will be parsed from [`parser`]'s
/// data instead.
///
/// Returns results of parsing, described by [`Command.CallData`].
/// Returned object is guaranteed to be not `none`.
public final function Command.CallData ParseWith(
Parser parser,
Command.Data commandData,
EPlayer callerPlayer,
optional BaseText specifiedSubCommand
) {
local HashTable commandParameters;
// Temporary object to return `nextResult` while setting variable to `none`
local Command.CallData toReturn;
nextResult.parameters = _.collections.EmptyHashTable();
nextResult.options = _.collections.EmptyHashTable();
if (commandData.subCommands.length == 0) {
DeclareError(CET_NoSubCommands, none);
toReturn = nextResult;
Reset();
return toReturn;
}
if (parser == none || !parser.Ok()) {
DeclareError(CET_BadParser, none);
toReturn = nextResult;
Reset();
return toReturn;
}
commandParser = parser;
availableOptions = commandData.options;
currentPlayersParser =
PlayersParser(_.memory.Allocate(class'PlayersParser'));
currentPlayersParser.SetSelf(callerPlayer);
// (subcommand) (parameters, possibly with options) and nothing else!
PickSubCommand(commandData, specifiedSubCommand);
nextResult.subCommandName = pickedSubCommand.name.Copy();
commandParameters = ParseParameterArrays(pickedSubCommand.required, pickedSubCommand.optional);
AssertNoTrailingInput(); // make sure there is nothing else
if (commandParser.Ok()) {
nextResult.parameters = commandParameters;
} else {
_.memory.Free(commandParameters);
}
// Clean up
toReturn = nextResult;
Reset();
return toReturn;
}
// Zero important variables // Zero important variables
private final function Reset() { private final function Reset() {
local Command.CallData blankCallData; local Command.CallData blankCallData;
@ -186,11 +253,11 @@ private final function DeclareError(Command.ErrorType type, optional BaseText ca
// Assumes `commandParser != none`, is in successful state. // Assumes `commandParser != none`, is in successful state.
// //
// Picks a sub command based on it's contents (parser's pointer must be before where subcommand's // Picks a sub command based on it's contents (parser's pointer must be before
// name is specified). // where subcommand's name is specified).
// //
// If `specifiedSubCommand` is not `none` - will always use that value instead of parsing it from // If [`specifiedSubCommand`] is not `none` - will always use that value instead
// `commandParser`. // of parsing it from [`commandParser`].
private final function PickSubCommand(Command.Data commandData, BaseText specifiedSubCommand) { private final function PickSubCommand(Command.Data commandData, BaseText specifiedSubCommand) {
local int i; local int i;
local MutableText candidateSubCommandName; local MutableText candidateSubCommandName;
@ -224,64 +291,11 @@ private final function PickSubCommand(Command.Data commandData, BaseText specifi
} }
} }
// We will only reach here if we did not match any sub commands, // We will only reach here if we did not match any sub commands,
// meaning that whatever consumed by `candidateSubCommandName` probably // meaning that whatever consumed by[ `candidateSubCommandName`] probably
// has a different meaning. // has a different meaning.
commandParser.RestoreState(confirmedState); commandParser.RestoreState(confirmedState);
} }
/// Parses user's input given in [`parser`] using command's information given by [`commandData`].
///
/// Optionally, sub-command can be specified for the [`CommandParser`] to use via
/// [`specifiedSubCommand`] argument.
/// If this argument's value is `none` - it will be parsed from `parser`'s data instead.
///
/// Returns results of parsing, described by `Command.CallData`.
/// Returned object is guaranteed to be not `none`.
public final function Command.CallData ParseWith(
Parser parser,
Command.Data commandData,
EPlayer callerPlayer,
optional BaseText specifiedSubCommand
) {
local HashTable commandParameters;
// Temporary object to return `nextResult` while setting variable to `none`
local Command.CallData toReturn;
nextResult.parameters = _.collections.EmptyHashTable();
nextResult.options = _.collections.EmptyHashTable();
if (commandData.subCommands.length == 0) {
DeclareError(CET_NoSubCommands, none);
toReturn = nextResult;
Reset();
return toReturn;
}
if (parser == none || !parser.Ok()) {
DeclareError(CET_BadParser, none);
toReturn = nextResult;
Reset();
return toReturn;
}
commandParser = parser;
availableOptions = commandData.options;
currentPlayersParser =
PlayersParser(_.memory.Allocate(class'PlayersParser'));
currentPlayersParser.SetSelf(callerPlayer);
// (subcommand) (parameters, possibly with options) and nothing else!
PickSubCommand(commandData, specifiedSubCommand);
nextResult.subCommandName = pickedSubCommand.name.Copy();
commandParameters = ParseParameterArrays(pickedSubCommand.required, pickedSubCommand.optional);
AssertNoTrailingInput(); // make sure there is nothing else
if (commandParser.Ok()) {
nextResult.parameters = commandParameters;
} else {
_.memory.Free(commandParameters);
}
// Clean up
toReturn = nextResult;
Reset();
return toReturn;
}
// Assumes `commandParser` is not `none` // Assumes `commandParser` is not `none`
// Declares an error if `commandParser` still has any input left // Declares an error if `commandParser` still has any input left
private final function AssertNoTrailingInput() { private final function AssertNoTrailingInput() {
@ -296,7 +310,8 @@ private final function AssertNoTrailingInput() {
} }
// Assumes `commandParser` is not `none`. // Assumes `commandParser` is not `none`.
// Parses given required and optional parameters along with any possible option declarations. // Parses given required and optional parameters along with any possible option
// declarations.
// Returns `HashTable` filled with (variable, parsed value) pairs. // Returns `HashTable` filled with (variable, parsed value) pairs.
// Failure is equal to `commandParser` entering into a failed state. // Failure is equal to `commandParser` entering into a failed state.
private final function HashTable ParseParameterArrays( private final function HashTable ParseParameterArrays(
@ -354,8 +369,8 @@ private final function ParseRequiredParameterArray(
// Assumes `commandParser` and `parsedParameters` are not `none`. // Assumes `commandParser` and `parsedParameters` are not `none`.
// //
// Parses given optional parameters along with any possible option declarations into given // Parses given optional parameters along with any possible option declarations
// `parsedParameters` hash table. // into given `parsedParameters` hash table.
private final function ParseOptionalParameterArray( private final function ParseOptionalParameterArray(
HashTable parsedParameters, HashTable parsedParameters,
array<Command.Parameter> optionalParameters array<Command.Parameter> optionalParameters
@ -384,10 +399,11 @@ private final function ParseOptionalParameterArray(
// Assumes `commandParser` and `parsedParameters` are not `none`. // Assumes `commandParser` and `parsedParameters` are not `none`.
// //
// Parses one given parameter along with any possible option declarations into given // Parses one given parameter along with any possible option declarations into
// `parsedParameters` `HashTable`. // given `parsedParameters` `HashTable`.
// //
// Returns `true` if we've successfully parsed given parameter without any errors. // Returns `true` if we've successfully parsed given parameter without any
// errors.
private final function bool ParseParameter( private final function bool ParseParameter(
HashTable parsedParameters, HashTable parsedParameters,
Command.Parameter expectedParameter Command.Parameter expectedParameter
@ -423,10 +439,12 @@ private final function bool ParseParameter(
// Assumes `commandParser` and `parsedParameters` are not `none`. // Assumes `commandParser` and `parsedParameters` are not `none`.
// //
// Parses a single value for a given parameter (e.g. one integer for integer or integer list // Parses a single value for a given parameter (e.g. one integer for integer or
// parameter types) along with any possible option declarations into given `parsedParameters`. // integer list parameter types) along with any possible option declarations
// into given `parsedParameters`.
// //
// Returns `true` if we've successfully parsed a single value without any errors. // Returns `true` if we've successfully parsed a single value without
// any errors.
private final function bool ParseSingleValue( private final function bool ParseSingleValue(
HashTable parsedParameters, HashTable parsedParameters,
Command.Parameter expectedParameter Command.Parameter expectedParameter
@ -537,7 +555,8 @@ private final function bool ParseIntegerValue(
} }
// Assumes `commandParser` and `parsedParameters` are not `none`. // Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single number (float) value into given `parsedParameters` hash table. // Parses a single number (float) value into given `parsedParameters`
// hash table.
private final function bool ParseNumberValue( private final function bool ParseNumberValue(
HashTable parsedParameters, HashTable parsedParameters,
Command.Parameter expectedParameter Command.Parameter expectedParameter
@ -591,8 +610,8 @@ private final function bool ParseTextValue(
return true; return true;
} }
// Resolves alias and returns it, along with the resolved value, if parameter was specified to be // Resolves alias and returns it, along with the resolved value, if parameter
// auto-resolved. // was specified to be auto-resolved.
// Returns `none` otherwise. // Returns `none` otherwise.
private final function HashTable AutoResolveAlias(MutableText textValue, Text aliasSourceName) { private final function HashTable AutoResolveAlias(MutableText textValue, Text aliasSourceName) {
local HashTable result; local HashTable result;
@ -628,8 +647,8 @@ private final function HashTable AutoResolveAlias(MutableText textValue, Text al
// Assumes `commandParser` and `parsedParameters` are not `none`. // Assumes `commandParser` and `parsedParameters` are not `none`.
// //
// Parses a single `Text` value into given `parsedParameters` hash table, consuming all remaining // Parses a single `Text` value into given `parsedParameters` hash table,
// contents. // consuming all remaining contents.
private final function bool ParseRemainderValue( private final function bool ParseRemainderValue(
HashTable parsedParameters, HashTable parsedParameters,
Command.Parameter expectedParameter Command.Parameter expectedParameter
@ -748,14 +767,15 @@ private final function RecordParameter(
// Assumes `commandParser` is not `none`. // Assumes `commandParser` is not `none`.
// //
// Tries to parse an option declaration (along with all of it's parameters) with `commandParser`. // Tries to parse an option declaration (along with all of it's parameters) with
// `commandParser`.
// //
// Returns `true` on success and `false` otherwise. // Returns `true` on success and `false` otherwise.
// //
// In case of failure to detect option declaration also reverts state of `commandParser` to that // In case of failure to detect option declaration also reverts state of
// before `TryParsingOptions()` call. // `commandParser` to that before `TryParsingOptions()` call.
// However, if option declaration was present, but invalid (or had invalid parameters) parser // However, if option declaration was present, but invalid (or had invalid
// will be left in a failed state. // parameters) parser will be left in a failed state.
private final function bool TryParsingOptions() { private final function bool TryParsingOptions() {
local int temporaryInt; local int temporaryInt;
@ -795,12 +815,12 @@ private final function bool TryParsingOptions() {
// Assumes `commandParser` is not `none`. // Assumes `commandParser` is not `none`.
// //
// Tries to parse a long option name along with all of it's possible parameters with // Tries to parse a long option name along with all of it's possible parameters
// `commandParser`. // with `commandParser`.
// //
// Returns `true` on success and `false` otherwise. At the point this method is called, option // Returns `true` on success and `false` otherwise. At the point this method is
// declaration is already assumed to be detected and any failure implies parsing error // called, option declaration is already assumed to be detected and any failure
// (ending in failed `Command.CallData`). // implies parsing error (ending in failed `Command.CallData`).
private final function bool ParseLongOption() { private final function bool ParseLongOption() {
local int i, optionIndex; local int i, optionIndex;
local MutableText optionName; local MutableText optionName;
@ -832,17 +852,16 @@ private final function bool ParseLongOption() {
// Assumes `commandParser` and `nextResult` are not `none`. // Assumes `commandParser` and `nextResult` are not `none`.
// //
// Tries to parse a short option name along with all of it's possible parameters with // Tries to parse a short option name along with all of it's possible parameters
// `commandParser`. // with `commandParser`.
// //
// Returns `true` on success and `false` otherwise. At the point this // Returns `true` on success and `false` otherwise. At the point this
// method is called, option declaration is already assumed to be detected // method is called, option declaration is already assumed to be detected
// and any failure implies parsing error (ending in failed `Command.CallData`). // and any failure implies parsing error (ending in failed `Command.CallData`).
private final function bool ParseShortOption() private final function bool ParseShortOption() {
{ local int i;
local int i; local bool pickedOptionWithParameters;
local bool pickedOptionWithParameters; local MutableText optionsList;
local MutableText optionsList;
commandParser.MUntil(optionsList,, true); commandParser.MUntil(optionsList,, true);
if (!commandParser.Ok()) { if (!commandParser.Ok()) {
@ -864,24 +883,28 @@ private final function bool ParseShortOption()
// Assumes `commandParser` and `nextResult` are not `none`. // Assumes `commandParser` and `nextResult` are not `none`.
// //
// Auxiliary method that adds option by it's short version's character `optionCharacter`. // Auxiliary method that adds option by it's short version's character
// `optionCharacter`.
// //
// It also accepts `optionSourceList` that describes short option expression (e.g. "-rtV") from // It also accepts `optionSourceList` that describes short option expression
// which it originated for error reporting and `forbidOptionWithParameters` that, when set to // (e.g. "-rtV") from
// `true`, forces this method to cause the `CET_MultipleOptionsWithParams` error if new option has // which it originated for error reporting and `forbidOptionWithParameters`
// non-empty parameters. // that, when set to `true`, forces this method to cause the
// `CET_MultipleOptionsWithParams` error if new option has non-empty parameters.
// //
// Method returns `true` if added option had non-empty parameters and `false` otherwise. // Method returns `true` if added option had non-empty parameters and `false`
// otherwise.
// //
// Any parsing failure inside this method always causes `nextError.DeclareError()` call, so you // Any parsing failure inside this method always causes
// can use `nextResult.IsSuccessful()` to check if method has failed. // `nextError.DeclareError()` call, so you can use `nextResult.IsSuccessful()`
// to check if method has failed.
private final function bool AddOptionByCharacter( private final function bool AddOptionByCharacter(
BaseText.Character optionCharacter, BaseText.Character optionCharacter,
BaseText optionSourceList, BaseText optionSourceList,
bool forbidOptionWithParameters bool forbidOptionWithParameters
) { ) {
local int i; local int i;
local bool optionHasParameters; local bool optionHasParameters;
// Prevent same option appearing twice // Prevent same option appearing twice
for (i = 0; i < usedOptions.length; i += 1) { for (i = 0; i < usedOptions.length; i += 1) {
@ -915,7 +938,8 @@ private final function bool AddOptionByCharacter(
} }
// Auxiliary method for parsing option's parameters (including empty ones). // Auxiliary method for parsing option's parameters (including empty ones).
// Automatically fills `nextResult` with parsed parameters (or `none` if option has no parameters). // Automatically fills `nextResult` with parsed parameters (or `none` if option
// has no parameters).
// Assumes `commandParser` and `nextResult` are not `none`. // Assumes `commandParser` and `nextResult` are not `none`.
private final function bool ParseOptionParameters(Command.Option pickedOption) { private final function bool ParseOptionParameters(Command.Option pickedOption) {
local HashTable optionParameters; local HashTable optionParameters;
@ -946,13 +970,13 @@ private final function bool ParseOptionParameters(Command.Option pickedOption) {
} }
defaultproperties { defaultproperties {
booleanTrueEquivalents(0) = "true" booleanTrueEquivalents(0) = "true"
booleanTrueEquivalents(1) = "enable" booleanTrueEquivalents(1) = "enable"
booleanTrueEquivalents(2) = "on" booleanTrueEquivalents(2) = "on"
booleanTrueEquivalents(3) = "yes" booleanTrueEquivalents(3) = "yes"
booleanFalseEquivalents(0) = "false" booleanFalseEquivalents(0) = "false"
booleanFalseEquivalents(1) = "disable" booleanFalseEquivalents(1) = "disable"
booleanFalseEquivalents(2) = "off" booleanFalseEquivalents(2) = "off"
booleanFalseEquivalents(3) = "no" booleanFalseEquivalents(3) = "no"
errNoSubCommands = (l=LOG_Error,m="`GetSubCommand()` method was called on a command `%1` with zero defined sub-commands.") errNoSubCommands = (l=LOG_Error,m="`GetSubCommand()` method was called on a command `%1` with zero defined sub-commands.")
} }

255
sources/BaseAPI/API/Commands/PlayersParser.uc

@ -27,71 +27,75 @@ class PlayersParser extends AcediaObject
//! //!
//! Basic use is to specify one of the selectors: //! Basic use is to specify one of the selectors:
//! 1. Key selector: "#<integer>" (examples: "#1", "#5"). //! 1. Key selector: "#<integer>" (examples: "#1", "#5").
//! This one is used to specify players by their key, assigned to them when they enter the game. //! This one is used to specify players by their key, assigned to them when
//! they enter the game.
//! This type of selectors can be used when players have hard to type names. //! This type of selectors can be used when players have hard to type names.
//! 2. Macro selector: "@self", "@me", "@all", "@admin" or just "@". //! 2. Macro selector: "@self", "@me", "@all", "@admin" or just "@".
//! "@", "@me", and "@self" are identical and can be used to specify player that called //! "@", "@me", and "@self" are identical and can be used to specify player
//! the command. //! that called the command.
//! "@admin" can be used to specify all admins in the game at once. //! "@admin" can be used to specify all admins in the game at once.
//! "@all" specifies all current players. //! "@all" specifies all current players.
//! In future it is planned to make macros extendable by allowing to bind more names to specific //! In future it is planned to make macros extendable by allowing to bind
//! groups of players. //! more names to specific groups of players.
//! 3. Name selectors: quoted strings and any other types of string that do not start with //! 3. Name selectors: quoted strings and any other types of string that do not
//! either "#" or "@". //! start with either "#" or "@".
//! These specify name prefixes: any player with specified prefix will be considered to match //! These specify name prefixes: any player with specified prefix will be considered to match
//! such selector. //! such selector.
//! //!
//! Negated selectors: "!<selector>". Specifying "!" in front of selector will select all players //! Negated selectors: "!<selector>". Specifying "!" in front of selector will
//! that do not match it instead. //! select all players that do not match it instead.
//! //!
//! Grouped selectors: "['<selector1>', '<selector2>', ... '<selectorN>']". //! Grouped selectors: "['<selector1>', '<selector2>', ... '<selectorN>']".
//! Specified selectors are process in order: from left to right. //! Specified selectors are process in order: from left to right.
//! First selector works as usual and selects a set of players. //! First selector works as usual and selects a set of players.
//! All the following selectors either expand that list (additive ones, without "!" prefix) or //! All the following selectors either expand that list (additive ones, without
//! remove specific players from the list (the ones with "!" prefix). //! "!" prefix) or remove specific players from the list (the ones with "!"
//! Examples of that: //! prefix). Examples of that:
//! //!
//! * "[@admin, !@self]" - selects all admins, except the one who called the command //! * "[@admin, !@self]" - selects all admins, except the one who called
//! (whether he is admin or not). //! the command (whether he is admin or not).
//! * "[dkanus, 'mate']" - will select players "dkanus" and "mate". Order also matters, since: //! * "[dkanus, 'mate']" - will select players "dkanus" and "mate".
//! * "[@admin, !@admin]" - won't select anyone, since it will first add all the admins and //! Order also matters, since:
//! then remove them. //! - "[@admin, !@admin]" - won't select anyone, since it will first add all
//! * "[!@admin, @admin]" - will select everyone, since it will first select everyone who is //! the admins and then remove them.
//! not an admin and then adds everyone else. //! - "[!@admin, @admin]" - will select everyone, since it will first select
//! everyone who is not an admin and then adds everyone else.
//! //!
//! # Usage //! # Usage
//! //!
//! 1. Allocate `PlayerParser`; //! 1. Allocate `PlayerParser`;
//! 2. Set caller player through `SetSelf()` method to make "@" and "@me" selectors usable; //! 2. Set caller player through `SetSelf()` method to make "@" and "@me"
//! 3. Call `Parse()` or `ParseWith()` method with `BaseText`/`Parser` that starts with proper //! selectors usable;
//! players selector; //! 3. Call `Parse()` or `ParseWith()` method with `BaseText`/`Parser` that
//! starts with proper players selector;
//! 4. Call `GetPlayers()` to obtain selected players array. //! 4. Call `GetPlayers()` to obtain selected players array.
//! //!
//! # Implementation //! # Implementation
//! //!
//! When created, `PlayersParser` takes a snapshot (array) of current players on the server. //! When created, `PlayersParser` takes a snapshot (array) of current players on
//! Then `currentSelection` is decided based on whether first selector is positive //! the server. Then `currentSelection` is decided based on whether first
//! (initial selection is taken as empty array) or negative //! selector is positive (initial selection is taken as empty array) or negative
//! (initial selection is taken as full snapshot). //! (initial selection is taken as full snapshot).
//! //!
//! After that `PlayersParser` simply goes through specified selectors //! After that `PlayersParser` simply goes through specified selectors
//! (in case more than one is specified) and adds or removes appropriate players in //! (in case more than one is specified) and adds or removes appropriate players
//! `currentSelection`, assuming that `playersSnapshot` is a current full array of players. //! in `currentSelection`, assuming that `playersSnapshot` is a current full
//! array of players.
// Player for which "@", "@me", and "@self" macros will refer
var private EPlayer selfPlayer; /// Player for which "@", "@me", and "@self" macros will refer
// Copy of the list of current players at the moment of allocation of var private EPlayer selfPlayer;
// this `PlayersParser`. /// Copy of the list of current players at the moment of allocation of
var private array<EPlayer> playersSnapshot; /// this `PlayersParser`.
// Players, selected according to selectors we have parsed so far var private array<EPlayer> playersSnapshot;
var private array<EPlayer> currentSelection; /// Players, selected according to selectors we have parsed so far
// Have we parsed our first selector? var private array<EPlayer> currentSelection;
// We need this to know whether to start with the list of /// Have we parsed our first selector?
// all players (if first selector removes them) or /// We need this to know whether to start with the list of
// with empty list (if first selector adds them). /// all players (if first selector removes them) or
var private bool parsedFirstSelector; /// with empty list (if first selector adds them).
// Will be equal to a single-element array [","], used for parsing var private bool parsedFirstSelector;
var private array<Text> selectorDelimiters; /// Will be equal to a single-element array [","], used for parsing
var private array<Text> selectorDelimiters;
var const int TSELF, TME, TADMIN, TALL, TNOT, TKEY, TMACRO, TCOMMA; var const int TSELF, TME, TADMIN, TALL, TNOT, TKEY, TMACRO, TCOMMA;
var const int TOPEN_BRACKET, TCLOSE_BRACKET; var const int TOPEN_BRACKET, TCLOSE_BRACKET;
@ -118,8 +122,77 @@ public final function SetSelf(EPlayer newSelfPlayer) {
} }
} }
// Insert a new player into currently selected list of players (`currentSelection`) such that there /// Returns players parsed by the last `ParseWith()` or `Parse()` call.
// will be no duplicates. ///
/// If neither were yet called - returns an empty array.
public final function array<EPlayer> GetPlayers() {
local int i;
local array<EPlayer> result;
for (i = 0; i < currentSelection.length; i += 1) {
if (currentSelection[i].IsExistent()) {
result[result.length] = EPlayer(currentSelection[i].Copy());
}
}
return result;
}
/// Parses players from `parser` according to the currently present players.
///
/// Array of parsed players can be retrieved by `self.GetPlayers()` method.
///
/// Returns `true` if parsing was successful and `false` otherwise.
public final function bool ParseWith(Parser parser) {
local Parser.ParserState confirmedState;
if (parser == none) return false;
if (!parser.Ok()) return false;
if (parser.HasFinished()) return false;
Reset();
confirmedState = parser.Skip().GetCurrentState();
if (!parser.Match(T(TOPEN_BRACKET)).Ok()) {
ParseSelector(parser.RestoreState(confirmedState));
if (parser.Ok()) {
return true;
}
return false;
}
while (parser.Ok() && !parser.HasFinished()) {
confirmedState = parser.Skip().GetCurrentState();
if (parser.Match(T(TCLOSE_BRACKET)).Ok()) {
return true;
}
parser.RestoreState(confirmedState);
if (parsedFirstSelector) {
parser.Match(T(TCOMMA)).Skip();
}
ParseSelector(parser);
parser.Skip();
}
parser.Fail();
return false;
}
/// Parses players from according to the currently present players.
///
/// Array of parsed players can be retrieved by `self.GetPlayers()` method.
/// Returns `true` if parsing was successful and `false` otherwise.
public final function bool Parse(BaseText toParse) {
local bool wasSuccessful;
local Parser parser;
if (toParse == none) {
return false;
}
parser = _.text.Parse(toParse);
wasSuccessful = ParseWith(parser);
parser.FreeSelf();
return wasSuccessful;
}
// Insert a new player into currently selected list of players
// (`currentSelection`) such that there will be no duplicates.
// //
// `none` values are auto-discarded. // `none` values are auto-discarded.
private final function InsertPlayer(EPlayer toInsert) { private final function InsertPlayer(EPlayer toInsert) {
@ -261,7 +334,8 @@ private final function RemoveByMacro(BaseText macroText) {
} }
} }
// Parses one selector from `parser`, while accordingly modifying current player selection list. // Parses one selector from `parser`, while accordingly modifying current player
// selection list.
private final function ParseSelector(Parser parser) { private final function ParseSelector(Parser parser) {
local bool additiveSelector; local bool additiveSelector;
local Parser.ParserState confirmedState; local Parser.ParserState confirmedState;
@ -299,8 +373,8 @@ private final function ParseSelector(Parser parser) {
ParseNameSelector(parser, additiveSelector); ParseNameSelector(parser, additiveSelector);
} }
// Parse key selector (assuming "#" is already consumed), while accordingly modifying current player // Parse key selector (assuming "#" is already consumed), while accordingly
// selection list. // modifying current player selection list.
private final function ParseKeySelector(Parser parser, bool additiveSelector) { private final function ParseKeySelector(Parser parser, bool additiveSelector) {
local int key; local int key;
@ -315,8 +389,8 @@ private final function ParseKeySelector(Parser parser, bool additiveSelector) {
} }
} }
// Parse macro selector (assuming "@" is already consumed), while accordingly modifying current // Parse macro selector (assuming "@" is already consumed), while accordingly
// player selection list. // modifying current player selection list.
private final function ParseMacroSelector(Parser parser, bool additiveSelector) { private final function ParseMacroSelector(Parser parser, bool additiveSelector) {
local MutableText macroName; local MutableText macroName;
local Parser.ParserState confirmedState; local Parser.ParserState confirmedState;
@ -339,7 +413,8 @@ private final function ParseMacroSelector(Parser parser, bool additiveSelector)
_.memory.Free(macroName); _.memory.Free(macroName);
} }
// Parse name selector, while accordingly modifying current player selection list. // Parse name selector, while accordingly modifying current player selection
// list.
private final function ParseNameSelector(Parser parser, bool additiveSelector) { private final function ParseNameSelector(Parser parser, bool additiveSelector) {
local MutableText playerName; local MutableText playerName;
local Parser.ParserState confirmedState; local Parser.ParserState confirmedState;
@ -362,10 +437,11 @@ private final function ParseNameSelector(Parser parser, bool additiveSelector) {
_.memory.Free(playerName); _.memory.Free(playerName);
} }
// Reads a string that can either be a body of name selector (some player's name prefix) or // Reads a string that can either be a body of name selector (some player's
// of a macro selector (what comes after "@"). // name prefix) or of a macro selector (what comes after "@").
// //
// This is different from `parser.MString()` because it also uses "," as a separator. // This is different from `parser.MString()` because it also uses "," as
// a separator.
private final function MutableText ParseLiteral(Parser parser) { private final function MutableText ParseLiteral(Parser parser) {
local MutableText literal; local MutableText literal;
local Parser.ParserState confirmedState; local Parser.ParserState confirmedState;
@ -381,60 +457,8 @@ private final function MutableText ParseLiteral(Parser parser) {
return literal; return literal;
} }
/// Returns players parsed by the last `ParseWith()` or `Parse()` call.
///
/// If neither were yet called - returns an empty array.
public final function array<EPlayer> GetPlayers() {
local int i;
local array<EPlayer> result;
for (i = 0; i < currentSelection.length; i += 1) {
if (currentSelection[i].IsExistent()) {
result[result.length] = EPlayer(currentSelection[i].Copy());
}
}
return result;
}
/// Parses players from `parser` according to the currently present players.
///
/// Array of parsed players can be retrieved by `self.GetPlayers()` method.
///
/// Returns `true` if parsing was successful and `false` otherwise.
public final function bool ParseWith(Parser parser) {
local Parser.ParserState confirmedState;
if (parser == none) return false;
if (!parser.Ok()) return false;
if (parser.HasFinished()) return false;
Reset();
confirmedState = parser.Skip().GetCurrentState();
if (!parser.Match(T(TOPEN_BRACKET)).Ok()) {
ParseSelector(parser.RestoreState(confirmedState));
if (parser.Ok()) {
return true;
}
return false;
}
while (parser.Ok() && !parser.HasFinished()) {
confirmedState = parser.Skip().GetCurrentState();
if (parser.Match(T(TCLOSE_BRACKET)).Ok()) {
return true;
}
parser.RestoreState(confirmedState);
if (parsedFirstSelector) {
parser.Match(T(TCOMMA)).Skip();
}
ParseSelector(parser);
parser.Skip();
}
parser.Fail();
return false;
}
// Resets this object to initial state before parsing and update // Resets this object to initial state before parsing and update
// `playersSnapshot` to contain current players. // `playersSnapshot` to contain current players.
private final function Reset() { private final function Reset() {
parsedFirstSelector = false; parsedFirstSelector = false;
currentSelection.length = 0; currentSelection.length = 0;
@ -446,23 +470,6 @@ private final function Reset() {
selectorDelimiters[1] = T(TCLOSE_BRACKET); selectorDelimiters[1] = T(TCLOSE_BRACKET);
} }
/// Parses players from according to the currently present players.
///
/// Array of parsed players can be retrieved by `self.GetPlayers()` method.
/// Returns `true` if parsing was successful and `false` otherwise.
public final function bool Parse(BaseText toParse) {
local bool wasSuccessful;
local Parser parser;
if (toParse == none) {
return false;
}
parser = _.text.Parse(toParse);
wasSuccessful = ParseWith(parser);
parser.FreeSelf();
return wasSuccessful;
}
defaultproperties { defaultproperties {
TSELF = 0 TSELF = 0
stringConstants(0) = "self" stringConstants(0) = "self"

Loading…
Cancel
Save