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.
 

607 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 use `Execute()` / `ExecuteFor()` to perform
* necessary actions when command is executed by a player.
* Copyright 2021 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(Text);
/**
* Possible errors that can arise when producing `CommandCall` 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
};
/**
* Possible types of parameters.
*/
enum ParameterType
{
CPT_Boolean,
CPT_Integer,
CPT_Number,
CPT_Text,
CPT_Object,
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 Text.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
{
var protected array<SubCommand> subCommands;
var protected array<Option> options;
var protected bool requiresTarget;
};
var private Data commandData;
// Default command name that will be used unless Acedia is configured to
// do otherwise
var private const string commandName;
// 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;
var public const int TSPACE, TCOMMAND_NAME_FALLBACK, TPLUS;
var public const int TOPEN_BRACKET, TCLOSE_BRACKET;
var public const int TKEY, TDOUBLE_KEY, TCOMMA_SPACE, TBOOLEAN, TINDENT;
var public const int TBOOLEAN_TRUE_FALSE, TBOOLEAN_ENABLE_DISABLE;
var public const int TBOOLEAN_ON_OFF, TBOOLEAN_YES_NO;
var public const int TOPTIONS, TCMD_WITH_TARGET, TCMD_WITHOUT_TARGET;
protected function Constructor()
{
local CommandDataBuilder dataBuilder;
dataBuilder =
CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
BuildData(dataBuilder);
commandData = dataBuilder.GetData();
dataBuilder.FreeSelf();
dataBuilder = none;
}
/**
* 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 what is needed when your command is called.
*
* @param callInfo Object filled with parameters that your command has
* been called with. Guaranteed to not be in error state.
*/
protected function Executed(CommandCall callInfo){}
/**
* Overload this method to perform what is needed 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 targetPlayer Player that this command must perform an action on.
* @param callInfo Object filled with parameters that your command has
* been called with. Guaranteed to not be in error state.
*/
protected function ExecutedFor(APlayer targetPlayer, CommandCall callInfo){}
/**
* 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;
}
/**
* Returns name (in lower case) of the caller command class.
*
* @return Name (in lower case) of the caller command class.
*/
public final static function Text GetName()
{
local Text name, lowerCaseName;
name = __().text.FromString(default.commandName);
lowerCaseName = name.LowerCopy();
name.FreeSelf();
return lowerCaseName;
}
/**
* Forces command to process (parse and, if successful, execute itself)
* player's input.
*
* @param parser Parser that contains player's input.
* @param callerPlayer Player that initiated this command's call.
* @return `CommandCall` object that described parsed command call.
* Guaranteed to be not `none`.
*/
public final function CommandCall ProcessInput(
Parser parser,
APlayer callerPlayer)
{
local int i;
local array<APlayer> targetPlayers;
local CommandParser commandParser;
local CommandCall callInfo;
if (parser == none || !parser.Ok()) {
return MakeAndReportError(callerPlayer, CET_BadParser);
}
// Parse targets and handle errors that can arise here
if (commandData.requiresTarget)
{
targetPlayers = ParseTargets(parser, callerPlayer);
if (!parser.Ok()) {
return MakeAndReportError(callerPlayer, CET_IncorrectTargetList);
}
if (targetPlayers.length <= 0) {
return MakeAndReportError(callerPlayer, CET_EmptyTargetList);
}
}
// Parse parameters themselves
commandParser = CommandParser(_.memory.Allocate(class'CommandParser'));
callInfo = commandParser.ParseWith(parser, commandData)
.SetCallerPlayer(callerPlayer)
.SetTargetPlayers(targetPlayers);
commandParser.FreeSelf();
// Report or execute
if (!callInfo.IsSuccessful())
{
ReportError(callerPlayer, callInfo);
return callInfo;
}
Executed(callInfo);
if (commandData.requiresTarget)
{
for (i = 0; i < targetPlayers.length; i += 1) {
ExecutedFor(targetPlayers[i], callInfo);
}
}
return callInfo;
}
// Reports given error to the `callerPlayer`, appropriately picking
// message color
private final function ReportError(
APLayer callerPlayer,
CommandCall callInfo)
{
local Text errorMessage;
local Color previousConsoleColor;
local ConsoleWriter console;
if (callerPlayer == none) return;
if (callInfo == none) return;
if (callInfo.IsSuccessful()) return;
// Setup console color
console = callerPlayer.Console();
previousConsoleColor = console.GetColor();
if (callInfo.GetError() == CET_EmptyTargetList) {
console.SetColor(_.color.TextWarning);
}
else {
console.SetColor(_.color.TextFailure);
}
// Send message
errorMessage = callInfo.PrintErrorMessage();
console.Write(errorMessage);
errorMessage.FreeSelf();
// Restore console color
console.SetColor(previousConsoleColor).Flush();
}
// Creates (and returns) empty `CommandCall` with given error type and
// empty error cause and reports it
private final function CommandCall MakeAndReportError(
APLayer callerPlayer,
ErrorType errorType)
{
local CommandCall dummyCall;
if (errorType == CET_None) return none;
dummyCall = class'CommandCall'.static.MakeError(errorType, callerPlayer);
ReportError(callerPlayer, dummyCall);
return dummyCall;
}
// Auxiliary method for parsing list of targeted players.
// Assumes given parser is not `none` and not in a failed state.
private final function array<APlayer> ParseTargets(
Parser parser,
APlayer callerPlayer)
{
local array<APlayer> targetPlayers;
local PlayersParser targetsParser;
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser'));
targetsParser.SetSelf(callerPlayer);
targetsParser.ParseWith(parser);
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 Text.Character GetNewLine(Text.Formatting formatting)
{
local Text.Character newLine;
newLine.codePoint = 10;
newLine.formatting = formatting;
return newLine;
}
/**
* Returns colored `Text` with auto-generated help page for the caller command.
*
* @return Auto-generated help page for the caller `Command` class.
*/
public final function Text PrintHelp()
{
local Text result, commandNameAsText, commandNameRandomCase;
local MutableText builder, subBuilder;
local Text.Formatting defaultFormatting;
defaultFormatting = _.text.FormattingFromColor(_.color.TextDefault);
builder = _.text.Empty();
// Get capitalized command name
commandNameRandomCase = _.text.FromString(commandName);
commandNameAsText = commandNameRandomCase.UpperCopy();
commandNameRandomCase.FreeSelf();
// Print header: name + basic info
builder.Append(commandNameAsText, defaultFormatting);
if (commandData.requiresTarget) {
builder.Append(T(TCMD_WITH_TARGET), defaultFormatting);
}
else {
builder.Append(T(TCMD_WITHOUT_TARGET), defaultFormatting);
}
// Print commands part
subBuilder = PrintCommands(commandNameAsText);
builder.Append(subBuilder);
_.memory.Free(subBuilder);
// Print options part
subBuilder = PrintOptions();
builder.Append(subBuilder);
_.memory.Free(subBuilder);
result = builder.Copy();
builder.FreeSelf();
return result;
}
private final function MutableText PrintCommands(Text commandNameAsText)
{
local int i;
local Text.Character newLine;
local MutableText builder, subBuilder;
local array<SubCommand> subCommands;
newLine = GetNewLine(_.text.FormattingFromColor(_.color.TextDefault));
subCommands = commandData.subCommands;
builder = _.text.Empty();
for (i = 0; i < subCommands.length; i += 1)
{
builder.AppendCharacter(newLine);
subBuilder = PrintSubCommand(commandNameAsText, subCommands[i]);
builder.AppendCharacter(newLine).Append(subBuilder);
_.memory.Free(subBuilder);
}
return builder;
}
private final function MutableText PrintOptions()
{
local int i;
local Text.Character newLine;
local MutableText builder, subBuilder;
local array<Option> options;
options = commandData.options;
if (options.length <= 0) {
return none;
}
newLine = GetNewLine(_.text.FormattingFromColor(_.color.TextDefault));
builder = _.text.Empty();
builder.AppendCharacter(newLine)
.Append(T(TOPTIONS))
.AppendCharacter(newLine);
for (i = 0; i < options.length; i += 1)
{
subBuilder = PrintOption(options[i]);
builder.AppendCharacter(newLine).Append(subBuilder);
_.memory.Free(subBuilder);
}
return builder;
}
private final function MutableText PrintSubCommand(
Text usedCommandName,
SubCommand subCommand)
{
local MutableText builder, subBuilder;
local Text.Formatting defaultFormatting, emphasisFormatting;
defaultFormatting = _.text.FormattingFromColor(_.color.TextDefault);
emphasisFormatting = _.text.FormattingFromColor(_.color.TextEmphasis);
// Command + parameters
builder = _.text.Empty().Append(usedCommandName, emphasisFormatting);
if (subCommand.name != none && !subCommand.name.IsEmpty())
{
builder.Append(T(TSPACE), defaultFormatting)
.Append(subCommand.name, emphasisFormatting);
}
subBuilder = PrintParameters(subCommand.required, subCommand.optional);
builder.Append(T(TSPACE), defaultFormatting).Append(subBuilder);
_.memory.Free(subBuilder);
// Text description
builder.AppendCharacter(GetNewLine(defaultFormatting))
.Append(T(TINDENT), defaultFormatting)
.Append(subCommand.description, defaultFormatting);
return builder;
}
private final function MutableText PrintOption(Option option)
{
local Text.Character shortName;
local MutableText builder, subBuilder;
local Text.Formatting defaultFormatting, emphasisFormatting;
defaultFormatting = _.text.FormattingFromColor(_.color.TextDefault);
emphasisFormatting = _.text.FormattingFromColor(_.color.TextEmphasis);
// Option name
shortName = option.shortName;
shortName.formatting = emphasisFormatting;
builder = _.text.Empty()
.Append(T(TKEY), emphasisFormatting) // "-"
.AppendCharacter(shortName)
.Append(T(TCOMMA_SPACE), defaultFormatting) //", "
.Append(T(TDOUBLE_KEY), emphasisFormatting) //"--"
.Append(option.longName, emphasisFormatting)
.Append(T(TSPACE), defaultFormatting);
// Possible options
if (option.required.length != 0 || option.optional.length != 0)
{
subBuilder = PrintParameters(option.required, option.optional);
builder.Append(subBuilder);
_.memory.Free(subBuilder);
// If there actually were options - start a new line
builder.AppendCharacter(GetNewLine(defaultFormatting))
.Append(T(TINDENT), defaultFormatting);
}
// Text description
return builder.Append(option.description, defaultFormatting);
}
private final function MutableText PrintParameters(
array<Parameter> required,
array<Parameter> optional)
{
local int i;
local MutableText builder, subBuilder;
local Text.Formatting defaultFormatting;
defaultFormatting = _.text.FormattingFromColor(_.color.TextDefault);
builder = _.text.Empty();
// Print required
for (i = 0; i < required.length; i += 1)
{
subBuilder = PrintParameter(required[i]);
builder.Append(subBuilder);
_.memory.Free(subBuilder);
if (i < required.length - 1) {
builder.Append(T(TSPACE), defaultFormatting);
}
}
if (optional.length <= 0) {
return builder;
}
// Print optional
builder.Append(T(TSPACE), defaultFormatting)
.Append(T(TOPEN_BRACKET), defaultFormatting);
for (i = 0; i < optional.length; i += 1)
{
subBuilder = PrintParameter(optional[i]);
builder.Append(subBuilder);
_.memory.Free(subBuilder);
if (i < optional.length - 1) {
builder.Append(T(TSPACE), defaultFormatting);
}
}
builder.Append(T(TCLOSE_BRACKET), defaultFormatting);
return builder;
}
private final function MutableText PrintParameter(Parameter parameter)
{
local MutableText builder;
local Text.Formatting defaultFormatting, typeFormatting;
defaultFormatting = _.text.FormattingFromColor(_.color.TextDefault);
switch (parameter.type)
{
case CPT_Boolean:
typeFormatting = _.text.FormattingFromColor(_.color.TypeBoolean);
break;
case CPT_Integer:
typeFormatting = _.text.FormattingFromColor(_.color.TypeNumber);
break;
case CPT_Number:
typeFormatting = _.text.FormattingFromColor(_.color.TypeNumber);
break;
case CPT_Text:
typeFormatting = _.text.FormattingFromColor(_.color.TypeString);
break;
case CPT_Object:
typeFormatting = _.text.FormattingFromColor(_.color.TypeLiteral);
break;
case CPT_Array:
typeFormatting = _.text.FormattingFromColor(_.color.TypeLiteral);
break;
default:
}
builder = _.text.Empty().Append(parameter.displayName, typeFormatting);
if (parameter.allowsList) {
builder.Append(T(TPLUS), typeFormatting);
}
return builder;
}
private final function Text PrintBooleanType(PreferredBooleanFormat booleanType)
{
switch (booleanType)
{
case PBF_TrueFalse:
return T(TBOOLEAN_TRUE_FALSE);
case PBF_EnableDisable:
return T(TBOOLEAN_ENABLE_DISABLE);
case PBF_OnOff:
return T(TBOOLEAN_ON_OFF);
case PBF_YesNo:
return T(TBOOLEAN_YES_NO);
default:
}
return T(TBOOLEAN);
}
defaultproperties
{
TSPACE = 0
stringConstants(0) = " "
TPLUS = 1
stringConstants(1) = "(+)"
TOPEN_BRACKET = 2
stringConstants(2) = "["
TCLOSE_BRACKET = 3
stringConstants(3) = "]"
TKEY = 4
stringConstants(4) = "-"
TDOUBLE_KEY = 5
stringConstants(5) = "--"
TCOMMA_SPACE = 6
stringConstants(6) = ", "
TINDENT = 7
stringConstants(7) = " "
TBOOLEAN = 8
stringConstants(8) = "boolean"
TBOOLEAN_TRUE_FALSE = 9
stringConstants(9) = "true/false"
TBOOLEAN_ENABLE_DISABLE = 10
stringConstants(10) = "enable/disable"
TBOOLEAN_ON_OFF = 11
stringConstants(11) = "on/off"
TBOOLEAN_YES_NO = 12
stringConstants(12) = "yes/no"
TCMD_WITH_TARGET = 13
stringConstants(13) = ": This command requires target to be specified."
TCMD_WITHOUT_TARGET = 14
stringConstants(14) = ": This command does not require target to be specified."
TOPTIONS = 15
stringConstants(15) = "OPTIONS"
// Under normal conditions we only create one instance of each, so
// there is no need to object pools
usesObjectPool = false
}