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.
608 lines
20 KiB
608 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. |
|
* Guaranteed to be not `none`. |
|
*/ |
|
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 |
|
} |