UnrealScript library and basis for all Acedia Framework mods
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

599 lines
20 KiB

/**
* This class is meant to represent a command type: to create new command
* one should extend it, then simply define required sub-commands/options and
* parameters in `BuildData()` and overload `Execute()` / `ExecuteFor()`
* to perform required actions when command is executed by a player.
* `Execute()` is called first, whenever command is executed and
* `ExecuteFor()` is called only for targeted commands, once for each
* targeted player.
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Command extends AcediaObject
dependson(BaseText);
/**
* Possible errors that can arise when parsing command parameters from
* user input
*/
enum ErrorType
{
// No error
CET_None,
// Bad parser was provided to parse user input
// (this should not be possible)
CET_BadParser,
// Sub-command name was not specified or was incorrect
// (this should not be possible)
CET_NoSubCommands,
// Required param for command / option was not specified
CET_NoRequiredParam,
CET_NoRequiredParamForOption,
// Unknown option key was specified
CET_UnknownOption,
CET_UnknownShortOption,
// Same option appeared twice in one command call
CET_RepeatedOption,
// Part of user's input could not be interpreted as a part of
// command's call
CET_UnusedCommandParameters,
// In one short option specification (e.g. '-lah') several options
// require parameters: this introduces ambiguity and is not allowed
CET_MultipleOptionsWithParams,
// (For targeted commands only)
// Targets are specified incorrectly (or none actually specified)
CET_IncorrectTargetList,
CET_EmptyTargetList
};
/**
* Structure that contains all the information about how `Command` was called.
*/
struct CallData
{
// Targeted players (if applicable)
var public array<EPlayer> targetPlayers;
// Specified sub-command and parameters/options
var public Text subCommandName;
// Provided parameters and specified options
var public AssociativeArray parameters;
var public AssociativeArray options;
// Errors that occurred during command call processing are described by
// error type and optional error textual name of the object
// (parameter, option, etc.) that caused it.
var public ErrorType parsingError;
var public Text errorCause;
};
/**
* Possible types of parameters.
*/
enum ParameterType
{
// Parses into `BoolBox`
CPT_Boolean,
// Parses into `IntBox`
CPT_Integer,
// Parses into `FloatBox`
CPT_Number,
// Parses into `Text`
CPT_Text,
// Special parameter that consumes the rest of the input into `Text`
CPT_Remainder,
// Parses into `AssociativeArray`
CPT_Object,
// Parses into `DynamicArray`
CPT_Array
};
/**
* Possible forms a boolean variable can be used as.
* Boolean parameter can define it's preferred format, which will be used
* for help page generation.
*/
enum PreferredBooleanFormat
{
PBF_TrueFalse,
PBF_EnableDisable,
PBF_OnOff,
PBF_YesNo
};
// Defines a singular command parameter
struct Parameter
{
// Display name (for the needs of help page displaying)
var Text displayName;
// Type of value this parameter would store
var ParameterType type;
// Does it take only a singular value or can it contain several of them,
// written in a list
var bool allowsList;
// Variable name that will be used as a key to store parameter's value
var Text variableName;
// (For `CPT_Boolean` type variables only) - preferred boolean format,
// used in help pages
var PreferredBooleanFormat booleanFormat;
};
// Defines a sub-command of a this command (specified as
// "<command> <sub_command>").
// Using sub-command is not optional, but if none defined
// (in `BuildData()`) / specified by the player - an empty (`name.IsEmpty()`)
// one is automatically created / used.
struct SubCommand
{
// Cannot be `none`
var Text name;
// Can be `none`
var Text description;
var array<Parameter> required;
var array<Parameter> optional;
};
// Defines command's option (options are specified by "--long" or "-l").
// Options are independent from sub-commands.
struct Option
{
var BaseText.Character shortName;
var Text longName;
var Text description;
// Option can also have their own parameters
var array<Parameter> required;
var array<Parameter> optional;
};
// Structure that defines what sub-commands and options command has
// (and what parameters they take)
struct Data
{
// Default command name that will be used unless Acedia is configured to
// do otherwise
var protected Text name;
// Short summary of what command does (recommended to
// keep it to 80 characters)
var protected Text summary;
var protected array<SubCommand> subCommands;
var protected array<Option> options;
var protected bool requiresTarget;
};
var private Data commandData;
// We do not really ever need to create more than one instance of each class
// of `Command`, so we will simply store and reuse one created instance.
var private Command mainInstance;
/**
* When command is being executed we create several instances of
* `ConsoleWriter` that can be used for command output. They will also be
* automatically deallocated once command is executed.
* DO NOT modify them or deallocate any of them manually.
* This should make output more convenient and standardized.
*
* 1. `publicConsole` - sends messages to all present players;
* 2. `callerConsole` - sends messages to the player that
* called the command;
* 3. `targetConsole` - sends messages to the player that is currently
* being targeted (different each call of `ExecutedFor()` and
* `none` during `Executed()` call);
* 4. `othersConsole` - sends messaged to every player that is
* neither "caller" or "target".
*/
var protected ConsoleWriter publicConsole, othersConsole;
var protected ConsoleWriter callerConsole, targetConsole;
protected function Constructor()
{
local CommandDataBuilder dataBuilder;
dataBuilder =
CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
BuildData(dataBuilder);
commandData = dataBuilder.GetData();
dataBuilder.FreeSelf();
dataBuilder = none;
}
protected function Finalizer()
{
local int i;
local array<SubCommand> subCommands;
local array<Option> options;
DeallocateConsoles();
_.memory.Free(commandData.name);
_.memory.Free(commandData.summary);
subCommands = commandData.subCommands;
for (i = 0; i < options.length; i += 1)
{
_.memory.Free(subCommands[i].name);
_.memory.Free(subCommands[i].description);
CleanParameters(subCommands[i].required);
CleanParameters(subCommands[i].optional);
subCommands[i].required.length = 0;
subCommands[i].optional.length = 0;
}
commandData.subCommands.length = 0;
options = commandData.options;
for (i = 0; i < options.length; i += 1)
{
_.memory.Free(options[i].longName);
_.memory.Free(options[i].description);
CleanParameters(options[i].required);
CleanParameters(options[i].optional);
options[i].required.length = 0;
options[i].optional.length = 0;
}
commandData.options.length = 0;
}
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);
}
}
/**
* Overload this method to use `builder` to define parameters and options for
* your command.
*
* @param builder Builder that can be used to define your commands parameters
* and options. Do not deallocate.
*/
protected function BuildData(CommandDataBuilder builder){}
/**
* Overload this method to perform required actions when
* your command is called.
*
* @param callData `struct` filled with parameters that your command
* has been called with. Guaranteed to not be in error state.
* @param instigator Player that instigated this execution.
*/
protected function Executed(CallData callData, EPlayer instigator){}
/**
* Overload this method to perform required actions when your command is called
* with a given player as a target. If several players have been specified -
* this method will be called once for each.
*
* If your command does not require a target - this method will not be called.
*
* @param target Player that this command must perform an action on.
* @param callData `struct` filled with parameters that your command
* has been called with. Guaranteed to not be in error state and contain
* all the required data.
* @param instigator Player that instigated this call.
*/
protected function ExecutedFor(
EPlayer target,
CallData callData,
EPlayer instigator){}
/**
* Returns an instance of command (of particular class) that is stored
* "as a singleton" in command's class itself. Do not deallocate it.
*/
public final static function Command GetInstance()
{
if (default.mainInstance == none) {
default.mainInstance = Command(__().memory.Allocate(default.class));
}
return default.mainInstance;
}
/**
* Forces command to process (parse) player's input, producing a structure
* with parsed data in Acedia's format instead.
*
* @see `Execute()` for actually performing command's actions.
*
* @param parser Parser that contains command input.
* @param callerPlayer Player that initiated this command's call,
* necessary for parsing player list (since it can point at
* the caller player).
* @return `CallData` structure that contains all the information about
* parameters specified in `parser`'s contents.
* Returned structure contains objects that must be deallocated,
* which can easily be done by the auxiliary `DeallocateCallData()` method.
*/
public final function CallData ParseInputWith(
Parser parser,
EPlayer callerPlayer)
{
local array<EPlayer> targetPlayers;
local CommandParser commandParser;
local CallData callData;
if (parser == none || !parser.Ok())
{
callData.parsingError = CET_BadParser;
return callData;
}
// Parse targets and handle errors that can arise here
if (commandData.requiresTarget)
{
targetPlayers = ParseTargets(parser, callerPlayer);
if (!parser.Ok())
{
callData.parsingError = CET_IncorrectTargetList;
return callData;
}
if (targetPlayers.length <= 0)
{
callData.parsingError = CET_EmptyTargetList;
return callData;
}
}
// Parse parameters themselves
commandParser = CommandParser(_.memory.Allocate(class'CommandParser'));
callData = commandParser.ParseWith(parser, commandData);
callData.targetPlayers = targetPlayers;
commandParser.FreeSelf();
return callData;
}
/**
* Executes caller `Command` with data provided by `callData` if it is in
* a correct state and reports error to `callerPlayer` if
* `callData` is invalid.
*
* @param callData Data about parameters, options, etc. with which
* caller `Command` is to be executed.
* @param callerPlayer Player that should be considered responsible for
* executing this `Command`.
* @return `true` if command was successfully executed and `false` otherwise.
* Execution is considered successful if `Execute()` call was made,
* regardless of whether `Command` can actually perform required action.
* For example, giving a weapon to a player can fail because he does not
* have enough space in his inventory, but it will still be considered
* a successful execution as far as return value is concerned.
*/
public final function bool Execute(CallData callData, EPlayer callerPlayer)
{
local int i;
local array<EPlayer> targetPlayers;
if (callerPlayer == none) return false;
if (!callerPlayer.IsExistent()) return false;
// Report or execute
if (callData.parsingError != CET_None)
{
ReportError(callData, callerPlayer);
return false;
}
targetPlayers = callData.targetPlayers;
MakeConsoles(targetPlayers, callerPlayer);
callerConsole.Write(P("Executing command `"))
.Write(commandData.name)
.Say(P("`"));
Executed(callData, callerPlayer);
if (commandData.requiresTarget)
{
for (i = 0; i < targetPlayers.length; i += 1)
{
targetConsole = _.console.For(targetPlayers[i]);
ExecutedFor(targetPlayers[i], callData, callerPlayer);
_.memory.Free(targetConsole);
targetConsole = none;
}
}
DeallocateConsoles();
return true;
}
private final function MakeConsoles(
array<EPlayer> targetPlayers,
EPlayer callerPlayer)
{
local int i;
publicConsole = _.console.ForAll();
callerConsole = _.console.For(callerPlayer);
othersConsole = _.console.ForAll().ButPlayer(callerPlayer);
for (i = 0; i < targetPlayers.length; i += 1) {
othersConsole.ButPlayer(targetPlayers[i]);
}
}
private final function DeallocateConsoles()
{
if (publicConsole != none && publicConsole.IsAllocated()) {
_.memory.Free(publicConsole);
}
if (callerConsole != none && callerConsole.IsAllocated()) {
_.memory.Free(callerConsole);
}
if (targetConsole != none && targetConsole.IsAllocated()) {
_.memory.Free(targetConsole);
}
if (othersConsole != none && othersConsole.IsAllocated()) {
_.memory.Free(othersConsole);
}
publicConsole = none;
callerConsole = none;
targetConsole = none;
othersConsole = none;
}
/**
* Auxiliary method that cleans up all data and deallocates all objects inside
* provided `callData` structure.
*
* @param callData Structure to clean. All stored data will be cleared,
* meaning that `DeallocateCallData()` method takes ownership of
* this parameter.
*/
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;
}
}
// Reports given error to the `callerPlayer`, appropriately picking
// message color
private final function ReportError(CallData callData, EPlayer callerPlayer)
{
local Text errorMessage;
local ConsoleWriter console;
if (callerPlayer == none) return;
if (!callerPlayer.IsExistent()) return;
// Setup console color
console = callerPlayer.BorrowConsole();
if (callData.parsingError == CET_EmptyTargetList) {
console.UseColor(_.color.textWarning);
}
else {
console.UseColor(_.color.textFailure);
}
// Send message
errorMessage = PrintErrorMessage(callData);
console.Say(errorMessage);
errorMessage.FreeSelf();
// Restore console color
console.ResetColor().Flush();
}
private final function Text PrintErrorMessage(CallData callData)
{
local Text result;
local MutableText builder;
builder = _.text.Empty();
switch (callData.parsingError)
{
case CET_BadParser:
builder.Append(P("Internal error occurred: invalid parser"));
break;
case CET_NoSubCommands:
builder.Append(P("Ill defined command: no subcommands"));
break;
case CET_NoRequiredParam:
builder.Append(P("Missing required parameter: "))
.Append(callData.errorCause);
break;
case CET_NoRequiredParamForOption:
builder.Append(P("Missing required parameter for option: "))
.Append(callData.errorCause);
break;
case CET_UnknownOption:
builder.Append(P("Invalid option specified: "))
.Append(callData.errorCause);
break;
case CET_UnknownShortOption:
builder.Append(P("Invalid short option specified"));
break;
case CET_RepeatedOption:
builder.Append(P("Option specified several times: "))
.Append(callData.errorCause);
break;
case CET_UnusedCommandParameters:
builder.Append(P("Part of command could not be parsed: "))
.Append(callData.errorCause);
break;
case CET_MultipleOptionsWithParams:
builder.Append(P( "Multiple short options in one declarations"
@ "require parameters: "))
.Append(callData.errorCause);
break;
case CET_IncorrectTargetList:
builder.Append(P("Target players are incorrectly specified."))
.Append(callData.errorCause);
break;
case CET_EmptyTargetList:
builder.Append(P("List of target players is empty"))
.Append(callData.errorCause);
break;
default:
}
result = builder.Copy();
builder.FreeSelf();
return result;
}
// Auxiliary method for parsing list of targeted players.
// Assumes given parser is not `none` and not in a failed state.
// If parsing failed, guaranteed to return an empty array.
private final function array<EPlayer> ParseTargets(
Parser parser,
EPlayer callerPlayer)
{
local array<EPlayer> targetPlayers;
local PlayersParser targetsParser;
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser'));
targetsParser.SetSelf(callerPlayer);
targetsParser.ParseWith(parser);
if (parser.Ok()) {
targetPlayers = targetsParser.GetPlayers();
}
targetsParser.FreeSelf();
return targetPlayers;
}
// TODO: This is a hack to insert new line symbol,
// this needs to be redone in a better way
private final function BaseText.Character GetNewLine(
BaseText.Formatting formatting)
{
local BaseText.Character newLine;
newLine.codePoint = 10;
newLine.formatting = formatting;
return newLine;
}
/**
* Returns name (in lower case) of the caller command class.
*
* @return Name (in lower case) of the caller command class.
* Guaranteed to be not `none`.
*/
public final function Text GetName()
{
if (commandData.name == none) {
return P("").Copy();
}
return commandData.name.LowerCopy();
}
// TODO: use `SharedRef` instead
/**
* Returns `Command.Data` struct that describes caller `Command`.
*
* @return `Command.Data` 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 GetData()
{
return commandData;
}
defaultproperties
{
}