Browse Source

Document commands-related classes

pull/8/head
Anton Tarasenko 2 years ago
parent
commit
cd056b9daa
  1. 50
      sources/Commands/BuiltInCommands/ACommandHelp.uc
  2. 69
      sources/Commands/Command.uc
  3. 40
      sources/Commands/CommandDataBuilder.uc
  4. 56
      sources/Commands/CommandParser.uc
  5. 52
      sources/Commands/Commands_Feature.uc
  6. 22
      sources/Commands/PlayersParser.uc

50
sources/Commands/BuiltInCommands/ACommandHelp.uc

@ -20,12 +20,58 @@
class ACommandHelp extends Command
dependson(LoggerAPI);
/**
* # `ACommandHelp`
*
* This built-in command is meant to do two things:
*
* 1. List all available commands and aliases related to them;
* 2. Display help pages for available commands (both by command and
* alias names).
*
* ## Implementation
*
* ### Aliases loading
*
* First thing this command tries to do upon creation is to read all
* command aliases and build a reverse map that allows to access aliases names
* by command + subcommand (even if latter one is empty) that these names
* refer to. This allows us to display relevant aliases names alongside
* command names. Map is stored inside `commandToAliasesMap` variable.
* (If aliases feature isn't yet available, `ACommandHelp` connects to
* `OnFeatureEnabled()` signal to do its job the moment required feature is
* enabled).
* This map is also made to be two-level one - it is...
*
* * a `HashTable` with command names as keys,
* * that points to `HashTable`s with subcommand names as keys,
* * that points at `ArrayList` of aliases.
*
* This allows us to also sort aliases by the subcommand name when displaying
* their list. This work is done by `FillCommandToAliasesMap()` method (that
* uses `ParseCommandNames()` method, which is also used for displaying
* command list).
*
* ### Command list displaying
*
* This is a simple process performed by `DisplayCommandLists()` that calls on
* a bunch of auxiliary `Print...()` methods.
*
* ### Displaying help pages
*
* Similar to the above, this is mostly a simple process that displays
* `Command`s' `CommandData` as a help page. Work is performed by
* `DisplayCommandHelpPages()` method that calls on a bunch of auxiliary
* `Print...()` methods, most notably `PrintHelpPageFor()` that can display
* help pages both for full commands, as well as only for a singular
* subcommand, which allows us to print proper help pages for command aliases
* that refer to a particular subcommand.
*/
// For each key (given by lower case command name) stores another `HashMap`
// that uses sub-command names as keys and returns `ArrayList` of aliases.
var private HashTable commandToAliasesMap;
var LoggerAPI.Definition testMsg;
var public const int TSPACE, TCOMMAND_NAME_FALLBACK, TPLUS;
var public const int TOPEN_BRACKET, TCLOSE_BRACKET, TCOLON_SPACE;
var public const int TKEY, TDOUBLE_KEY, TCOMMA_SPACE, TBOOLEAN, TINDENT;

69
sources/Commands/Command.uc

@ -26,6 +26,69 @@
class Command extends AcediaObject
dependson(BaseText);
/**
* # `Command`
*
* 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".
*
* ## Usage
*
* To create a custom command you need to simply:
*
* 1. Create a custom command class derived from `Command`;
* 2. Define `BuildData()` function and use given `CommandDataBuilder` to
* fill-in data about what parameters your command takes. You can also
* add optional descriptions that would appear in your command's
* help page.
* 3. Overload `Executed()` or `ExecutedFor()` (or both) method and add
* whatever logic you want to execute once your command was called.
* All parameters and options will be listed inside passed `CallData`
* parameter. These methods will only be called if all necessary
* parameters were correctly specified.
*
* ## 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
@ -227,6 +290,7 @@ protected function Finalizer()
local int i;
local array<SubCommand> subCommands;
local array<Option> options;
DeallocateConsoles();
_.memory.Free(commandData.name);
_.memory.Free(commandData.summary);
@ -257,6 +321,7 @@ protected function Finalizer()
private final function CleanParameters(array<Parameter> parameters)
{
local int i;
for (i = 0; i < parameters.length; i += 1)
{
_.memory.Free(parameters[i].displayName);
@ -340,6 +405,7 @@ public final function CallData ParseInputWith(
local array<EPlayer> targetPlayers;
local CommandParser commandParser;
local CallData callData;
if (parser == none || !parser.Ok())
{
callData.parsingError = CET_BadParser;
@ -479,6 +545,7 @@ private final function ReportError(CallData callData, EPlayer callerPlayer)
{
local Text errorMessage;
local ConsoleWriter console;
if (callerPlayer == none) return;
if (!callerPlayer.IsExistent()) return;
@ -502,6 +569,7 @@ private final function Text PrintErrorMessage(CallData callData)
{
local Text result;
local MutableText builder;
builder = _.text.Empty();
switch (callData.parsingError)
{
@ -567,6 +635,7 @@ private final function array<EPlayer> ParseTargets(
{
local array<EPlayer> targetPlayers;
local PlayersParser targetsParser;
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser'));
targetsParser.SetSelf(callerPlayer);
targetsParser.ParseWith(parser);

40
sources/Commands/CommandDataBuilder.uc

@ -22,11 +22,27 @@ class CommandDataBuilder extends AcediaObject
dependson(Command);
/**
* # `CommandDataBuilder`
*
* This class is made for convenient creation of `Command.Data` using
* a builder pattern.
*
* ## Usage
*
* `CommandDataBuilder` should be able to fill information about:
* 1. subcommands and their parameters;
* 2. options and their parameters.
* subcommands/options and their parameters.
* As far as user is concerned, the process of filling both should be
* identical. Therefore we will store all defined data in two ways:
* identical. Overall, intended flow for creating a new sub-command or option
* is to select either, fill it with data with public methods `Param...()` into
* "selected data" and then copy it into "prepared data"
* (through a `RecordSelection()` method below).
* For examples see `BuildData()` methods from `ACommandHelp` or any of
* the commands from "Futility" package.
*
* ## Implementation
*
* We will store all defined data in two ways:
*
* 1. Selected data: data about parameters for subcommand/option that is
* currently being filled;
* 2. Prepared data: data that was already filled as "selected data" then
@ -35,10 +51,8 @@ class CommandDataBuilder extends AcediaObject
* dump "selected data" into "prepared data" first and then return
* the latter.
*
* Overall, intended flow for creating a new sub-command or option is to
* select either, fill it with data with public methods `Param...()` into
* "selected data" and then copy it into "prepared data"
* (through a `RecordSelection()` method below).
* Builder object is automatically created when new `Command` instance is
* allocated and, therefore, doesn't normally need to be allocated by hand.
*/
// "Prepared data"
@ -100,6 +114,7 @@ protected function Finalizer()
private final function int FindSubCommandIndex(BaseText name)
{
local int i;
if (name == none) {
return -1;
}
@ -118,6 +133,7 @@ private final function int FindSubCommandIndex(BaseText name)
private final function int FindOptionIndex(BaseText longName)
{
local int i;
if (longName == none) {
return -1;
}
@ -151,6 +167,7 @@ private final function MakeEmptySelection(BaseText name, bool selectedOption)
private final function SelectSubCommand(BaseText name)
{
local int subcommandIndex;
if (name == none) return;
if ( !selectedItemIsOption && selectedItemName != none
&& selectedItemName.Compare(name))
@ -186,6 +203,7 @@ private final function SelectSubCommand(BaseText name)
private final function SelectOption(BaseText longName)
{
local int optionIndex;
if (longName == none) return;
if ( selectedItemIsOption && selectedItemName != none
&& selectedItemName.Compare(longName))
@ -233,6 +251,7 @@ private final function RecordSelectedSubCommand()
{
local int selectedSubCommandIndex;
local Command.SubCommand newSubcommand;
if (selectedItemName == none) return;
selectedSubCommandIndex = FindSubCommandIndex(selectedItemName);
@ -261,6 +280,7 @@ private final function RecordSelectedOption()
{
local int selectedOptionIndex;
local Command.Option newOption;
if (selectedItemName == none) return;
selectedOptionIndex = FindOptionIndex(selectedItemName);
@ -347,6 +367,7 @@ private final function bool VerifyNoOptionNamingConflict(
BaseText.Character shortName)
{
local int i;
// To make sure we will search through the up-to-date `options`,
// record selection into prepared records.
RecordSelection();
@ -404,6 +425,7 @@ public final function CommandDataBuilder Option(
{
local int optionIndex;
local BaseText.Character shortNameAsCharacter;
// Unlike for `SubCommand()`, we need to ensure that option naming is
// correct and does not conflict with existing options
// (user might attempt to add two options with same long names and
@ -568,6 +590,7 @@ public final function CommandDataBuilder OptionalParams()
public final function Command.Data BorrowData()
{
local Command.Data newData;
RecordSelection();
newData.name = commandName;
newData.group = commandGroup;
@ -593,6 +616,7 @@ private final function Command.Parameter NewParameter(
optional BaseText variableName)
{
local Command.Parameter newParameter;
newParameter.displayName = displayName.Copy();
newParameter.type = parameterType;
newParameter.allowsList = isListParameter;
@ -632,6 +656,7 @@ public final function CommandDataBuilder ParamBoolean(
optional BaseText variableName)
{
local Command.Parameter newParam;
if (name == none) {
return self;
}
@ -667,6 +692,7 @@ public final function CommandDataBuilder ParamBooleanList(
optional BaseText variableName)
{
local Command.Parameter newParam;
if (name == none) {
return self;
}

56
sources/Commands/CommandParser.uc

@ -24,8 +24,43 @@ class CommandParser extends AcediaObject
dependson(Command);
/**
* `CommandParser` stores both it's state and command data, relevant to
* parsing, as it's member variables during the whole parsing process,
* # `CommandParser`
*
* Class specialized for parsing user input of the command's call into
* `Command.CallData` structure with the information about all parsed
* arguments.
* `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 - it won't do this hob for you;
* * List of targeted players must also be parsed using `PlayersParser` -
* `CommandParser` won't do this for you;
* * Optionally one can also decide on the referred subcommand and pass it
* into `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.
*
* However, above steps are handled by `Commands_Feature` and one only needs to
* call that feature's `HandleInput()` methods to pass user input with command
* call line there.
*
* ## Usage
*
* Allocate `CommandParser` and call `ParseWith()` method, providing it with:
*
* 1. `Parser`, filled with command call input;
* 2. Command's data that describes subcommands, options and their
* parameters for the command, which call we are parsing;
* 3. (Optionally) `EPlayer` reference to the player that initiated
* the command call;
* 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
*
* `CommandParser` stores both its state and command data, relevant to
* parsing, as its member variables during the whole parsing process,
* instead of passing that data around in every single method.
*
* We will give a brief overview of how around 20 parsing methods below
@ -43,6 +78,7 @@ class CommandParser extends AcediaObject
* it is called once for single-valued parameters, but possibly several times
* for list parameters that can contain several values.
* So main parsing method looks something like:
*
* ParseParameterArrays() {
* loop ParseParameter() {
* loop ParseSingleValue()
@ -235,6 +271,7 @@ public final function Command.CallData ParseWith(
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)
@ -279,6 +316,7 @@ public final function Command.CallData ParseWith(
private final function AssertNoTrailingInput()
{
local Text remainder;
if (!commandParser.Ok()) return;
if (commandParser.Skip().GetRemainingLength() <= 0) return;
@ -297,6 +335,7 @@ private final function HashTable ParseParameterArrays(
array<Command.Parameter> optionalParameters)
{
local HashTable parsedParameters;
if (!commandParser.Ok()) {
return none;
}
@ -317,6 +356,7 @@ private final function ParseRequiredParameterArray(
array<Command.Parameter> requiredParameters)
{
local int i;
if (!commandParser.Ok()) {
return;
}
@ -355,6 +395,7 @@ private final function ParseOptionalParameterArray(
array<Command.Parameter> optionalParameters)
{
local int i;
if (!commandParser.Ok()) {
return;
}
@ -387,6 +428,7 @@ private final function bool ParseParameter(
Command.Parameter expectedParameter)
{
local bool parsedEnough;
confirmedState = commandParser.GetCurrentState();
while (ParseSingleValue(parsedParameters, expectedParameter))
{
@ -494,6 +536,7 @@ private final function bool ParseBooleanValue(
local bool isValidBooleanLiteral;
local bool booleanValue;
local MutableText parsedLiteral;
commandParser.Skip().MUntil(parsedLiteral,, true);
if (!commandParser.Ok())
{
@ -538,6 +581,7 @@ private final function bool ParseIntegerValue(
Command.Parameter expectedParameter)
{
local int integerValue;
commandParser.Skip().MInteger(integerValue);
if (!commandParser.Ok()) {
return false;
@ -555,6 +599,7 @@ private final function bool ParseNumberValue(
Command.Parameter expectedParameter)
{
local float numberValue;
commandParser.Skip().MNumber(numberValue);
if (!commandParser.Ok()) {
return false;
@ -656,6 +701,7 @@ private final function bool ParseObjectValue(
Command.Parameter expectedParameter)
{
local HashTable objectValue;
objectValue = _.json.ParseHashTableWith(commandParser);
if (!commandParser.Ok()) {
return false;
@ -672,6 +718,7 @@ private final function bool ParseArrayValue(
Command.Parameter expectedParameter)
{
local ArrayList arrayValue;
arrayValue = _.json.ParseArrayListWith(commandParser);
if (!commandParser.Ok()) {
return false;
@ -761,6 +808,7 @@ private final function RecordParameter(
private final function bool TryParsingOptions()
{
local int temporaryInt;
if (!commandParser.Ok()) return false;
confirmedState = commandParser.GetCurrentState();
@ -803,6 +851,7 @@ private final function bool ParseLongOption()
{
local int i, optionIndex;
local MutableText optionName;
commandParser.MUntil(optionName,, true);
if (!commandParser.Ok()) {
return false;
@ -843,6 +892,7 @@ private final function bool ParseShortOption()
local int i;
local bool pickedOptionWithParameters;
local MutableText optionsList;
commandParser.MUntil(optionsList,, true);
if (!commandParser.Ok())
{
@ -881,6 +931,7 @@ private final function bool AddOptionByCharacter(
{
local int i;
local bool optionHasParameters;
// Prevent same option appearing twice
for (i = 0; i < usedOptions.length; i += 1)
{
@ -923,6 +974,7 @@ private final function bool AddOptionByCharacter(
private final function bool ParseOptionParameters(Command.Option pickedOption)
{
local HashTable optionParameters;
// If we are already parsing other option's parameters and did not finish
// parsing all required ones - we cannot start another option
if (currentTargetIsOption && currentTarget != CPT_ExtraParameter)

52
sources/Commands/Commands_Feature.uc

@ -22,6 +22,47 @@
*/
class Commands_Feature extends Feature;
/**
* # `Commands_Feature`
*
* This feature provides a mechanism to define commands that automatically
* parse their arguments into standard Acedia collection. It also allows to
* manage them (and specify limitation on how they can be called) in a
* centralized manner.
* Support command input from chat and "mutate" command.
*
* ## Usage
*
* Should be enabled like any other feature. Additionally support
* `EmergencyEnable()` enabling method that bypasses regular settings to allow
* admins to start this feature while forcefully enabling "mutate" command
* input method.
* Available configuration:
*
* 1. Whether to use command input from chat and what prefix is used to
* denote a command (by default "!");
* 2. Whether to use command input from "mutate" command.
*
* To add new commands into the system - get enabled instance of this
* feature and call its `RegisterCommand()` method to add your custom
* `Command` class. `RemoveCommand()` can also be used to de-register
* a command, if you need this for some reason.
*
* ## Implementation
*
* Implementation is simple: calling a method `RegisterCommand()` adds
* command into two caches `registeredCommands` for obtaining registered
* commands by name and `groupedCommands` for obtaining arrays of commands by
* their group name. These arrays are used for providing methods for fetching
* arrays of commands and obtaining pre-allocated `Command` instances by their
* name.
* Depending on settings, this feature also connects to corresponding
* signals for catching "mutate"/chat input, then it checks user-specified name
* for being an alias and picks correct command from `registeredCommands`.
* Emergency enabling this feature sets `emergencyEnabledMutate` flag that
* enforces connecting to the "mutate" input.
*/
// Delimiters that always separate command name from it's parameters
var private array<Text> commandDelimiters;
// Registered commands, recorded as (<command_name>, <command_instance>) pairs.
@ -540,17 +581,6 @@ private final function CommandCallPair ParseCommandCallPairWith(Parser parser)
return result;
}
/*// Contains name of the command to call plus, optionally,
// additional sub-command name.
// Normally sub-command name is parsed by the command itself, however
// command aliases can try to enforce one.
struct CommandCallPair
{
var MutableText commandName;
// In case it is enforced by an alias
var MutableText subCommandName;
}; */
private function bool HandleCommands(
EPlayer sender,
MutableText message,

22
sources/Commands/PlayersParser.uc

@ -22,6 +22,8 @@ class PlayersParser extends AcediaObject
dependson(Parser);
/**
* # `PlayersParser`
*
* This parser is supposed to parse player set definitions as they
* are used in commands.
* Basic use is to specify one of the selectors:
@ -59,6 +61,26 @@ class PlayersParser extends AcediaObject
* add all the admins and then remove them.
* *. "[!@admin, @admin]" - will select everyone, since it will first
* select everyone who is not an admin and then adds everyone else.
*
* ## Usage
*
* 1. Allocate `PlayerParser`;
* 2. Set caller player through `SetSelf()` method to make "@" and "@me"
* selectors usable;
* 3. Call `Parse()` or `ParseWith()` method with `BaseText`/`Parser` that
* starts with proper players selector;
* 4. Call `GetPlayers()` to obtain selected players array.
*
* ## Implementation
*
* When created, `PlayersParser` takes a snapshot (array) of current
* players on the server. Then `currentSelection` is decided based on whether
* first selector is positive (initial selection is taken as empty array) or
* negative (initial selection is taken as full snapshot).
* After that `PlayersParser` simply goes through specified selectors
* (in case more than one is specified) and adds or removes appropriate players
* in `currentSelection`, assuming that `playersSnapshot` is a current full
* array of players.
*/
// Player for which "@", "@me", and "@self" macros will refer

Loading…
Cancel
Save