/**
* Command for displaying help information about registered Acedia's commands.
* Copyright 2021-2023 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 .
*/
class ACommandHelp extends Command
dependson(LoggerAPI)
dependson(CommandAPI);
/**
* # `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 private User callerUser;
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;
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;
var public const int TSEPARATOR, TLIST_REGIRESTED_CMDS, TEMPTY_GROUP;
var public const int TALIASES_FOR, TEMPTY, TDOT, TNO_COMMAND_BEGIN;
var public const int TNO_COMMAND_END, TEMPTY_GROUP_BEGIN, TEMPTY_GROUP_END;
protected function Constructor()
{
local Feature preenabledInstance;
super.Constructor();
// We need to update aliases map every time aliases feature is reenabled
_.environment.OnFeatureEnabled(self).connect = FillCommandToAliasesMap;
// If `Aliases_Feature` is already enabled - read its command aliases now
preenabledInstance = class'Aliases_Feature'.static.GetEnabledInstance();
FillCommandToAliasesMap(Aliases_Feature(preenabledInstance));
_.memory.Free(preenabledInstance);
}
protected function Finalizer()
{
super.Finalizer();
_.memory.Free(commandToAliasesMap);
commandToAliasesMap = none;
}
protected function BuildData(CommandDataBuilder builder)
{
builder.Group(P("core"));
builder.Summary(P("Displays detailed information about available commands."));
builder.OptionalParams();
builder.ParamTextList(P("commands"));
builder.Option(P("aliases"));
builder.Describe(P("When displaying available commands, specifying this flag will additionally"
@ "make command to display all of their available aliases."));
builder.Option(P("list"));
builder.Describe(P("Display available commands. Optionally command groups can be specified and"
@ "then only commands from such groups will be listed. Otherwise all commands will"
@ "be displayed."));
builder.OptionalParams();
builder.ParamTextList(P("groups"));
}
protected function Executed(
Command.CallData callData,
EPlayer callerPlayer,
CommandPermissions permissions
) {
local bool printedSomething;
local HashTable parameters, options;
local ArrayList commandsToDisplay, commandGroupsToDisplay;
callerUser = callerPlayer.GetIdentity();
parameters = callData.parameters;
options = callData.options;
// Print command list if "--list" option was specified
if (options.HasKey(P("list")))
{
commandGroupsToDisplay = options.GetArrayListBy(P("/list/groups"));
DisplayCommandLists(
commandGroupsToDisplay,
options.HasKey(P("aliases")));
_.memory.Free(commandGroupsToDisplay);
printedSomething = true;
}
// Help pages.
// Only need to print them if:
// 1. Any commands are specified as parameters;
// 2. No commands or "--list" option was specified, then we want to
// print a help page for this command.
if (!options.HasKey(P("list")) || parameters.HasKey(P("commands")))
{
commandsToDisplay = parameters.GetArrayList(P("commands"));
DisplayCommandHelpPages(commandsToDisplay, printedSomething);
_.memory.Free(commandsToDisplay);
}
_.memory.Free(callerUser);
callerUser = none;
}
// If instance of the `Aliases_Feature` is passed as an argument (allowing this
// method to be used as a slot for `OnFeatureEnabled` signal) and
// `commandAliasSource` is available - empties current `commandToAliasesMap`
// and refills it with available command aliases.
private final function FillCommandToAliasesMap(Feature enabledFeature)
{
local int i;
local Text aliasValue;
local array availableAliases;
local BaseAliasSource commandAliasSource;
local MutableText commandName, subcommandName;
if (enabledFeature == none) return;
if (enabledFeature.class != class'Aliases_Feature') return;
commandAliasSource = _.alias.GetCommandSource();
if (commandAliasSource == none) return;
// Drop map built by previous aliases (if it was even build up until now)
_.memory.Free(commandToAliasesMap);
commandToAliasesMap = _.collections.EmptyHashTable();
// Construct a command -> alias map by iterating over aliases
availableAliases = commandAliasSource.GetAllAliases();
for (i = 0; i < availableAliases.length; i += 1)
{
aliasValue = _.alias.ResolveCommand(availableAliases[i]);
ParseCommandNames(aliasValue, commandName, subcommandName);
aliasValue.FreeSelf();
InsertIntoAliasesMap(commandName, subcommandName, availableAliases[i]);
commandName.FreeSelf();
subcommandName.FreeSelf();
commandName = none;
subcommandName = none;
}
// Clean up
_.memory.FreeMany(availableAliases);
commandAliasSource.FreeSelf();
}
// Breaks command name as it is intended to be specified in command aliases
// ("" or ".") into command and subcommand,
// returning both as `out` parameters.
private final function ParseCommandNames(
BaseText source,
out MutableText commandName,
out MutableText subcommandName)
{
local Parser valueParser;
if (source == none) {
return;
}
if (subcommandName != none) {
subcommandName.FreeSelf();
}
valueParser = source.Parse();
subcommandName = valueParser
.MUntil(commandName, T(TDOT).GetCharacter(0))
.Match(T(TDOT))
.GetRemainderM();
_.memory.Free(valueParser);
}
// Assumes that `commandToAliasesMap` and its arguments are not `none`.
private final function InsertIntoAliasesMap(
BaseText commandName,
BaseText subcommandName,
BaseText alias)
{
local Text commandKey, subcommandKey;
local HashTable subcommandToAliasesMap;
local ArrayList aliasesArray;
commandKey = commandName.LowerCopy();
subcommandKey = subcommandName.LowerCopy();
subcommandToAliasesMap = commandToAliasesMap.GetHashTable(commandKey);
if (subcommandToAliasesMap == none) {
subcommandToAliasesMap = _.collections.EmptyHashTable();
}
commandToAliasesMap.SetItem(commandKey, subcommandToAliasesMap);
aliasesArray = subcommandToAliasesMap.GetArrayList(subcommandKey);
if (aliasesArray == none) {
aliasesArray = _.collections.EmptyArrayList();
}
subcommandToAliasesMap.SetItem(subcommandKey, aliasesArray);
if (aliasesArray.Find(alias) < 0) {
aliasesArray.AddItem(alias);
}
// Cleaning up local references to collections and keys
subcommandToAliasesMap.FreeSelf();
aliasesArray.FreeSelf();
commandKey.FreeSelf();
subcommandKey.FreeSelf();
}
private final function DisplayCommandLists(
ArrayList commandGroupsToDisplay,
bool displayAliases)
{
local int i;
local array commandNames, groupsNames;
if (commandGroupsToDisplay == none) {
groupsNames = _.commands.GetGroupsNames();
}
else
{
for (i = 0; i < commandGroupsToDisplay.GetLength(); i += 1) {
groupsNames[groupsNames.length] = commandGroupsToDisplay.GetText(i);
}
}
callerConsole.WriteLine(T(TLIST_REGIRESTED_CMDS));
for (i = 0; i < groupsNames.length; i += 1)
{
if (groupsNames[i] == none) {
continue;
}
commandNames = _.commands.GetCommandNamesInGroup(groupsNames[i]);
if (commandNames.length > 0)
{
callerConsole.UseColorOnce(_.color.TextSubHeader);
if (groupsNames[i].IsEmpty()) {
callerConsole.WriteLine(T(TEMPTY_GROUP));
}
else {
callerConsole.WriteLine(groupsNames[i]);
}
PrintCommandsNamesArray(
commandNames,
displayAliases);
_.memory.FreeMany(commandNames);
} else {
callerConsole.UseColor(_.color.TextFailure);
callerConsole.Write(T(TEMPTY_GROUP_BEGIN));
callerConsole.Write(groupsNames[i]);
callerConsole.WriteLine(T(TEMPTY_GROUP_END));
callerConsole.ResetColor();
}
}
_.memory.FreeMany(groupsNames);
}
private final function PrintCommandsNamesArray(
array commandsNamesArray,
bool displayAliases
) {
local int i;
local Command.Data nextData;
local CommandAPI.CommandConfigInfo nextCommandPair;
for (i = 0; i < commandsNamesArray.length; i += 1)
{
nextCommandPair = _.commands.ResolveCommandForUser(
commandsNamesArray[i],
callerUser);
if (nextCommandPair.instance != none && !nextCommandPair.usageForbidden) {
nextData = nextCommandPair.instance.BorrowData();
callerConsole
.UseColorOnce(_.color.textEmphasis)
.Write(commandsNamesArray[i])
.Write(T(TCOLON_SPACE))
.WriteLine(nextData.summary);
if (displayAliases) {
PrintCommandAliases(commandsNamesArray[i]);
}
}
_.memory.Free(nextCommandPair.instance);
}
}
private final function PrintCommandAliases(BaseText commandName)
{
local CollectionIterator iter;
local Text commandKey;
local Text nextSubCommand;
local ArrayList nextAliasesArray;
local HashTable subCommandToAliasesMap;
if (commandName == none) return;
if (commandToAliasesMap == none) return;
commandKey = commandName.LowerCopy();
subCommandToAliasesMap = commandToAliasesMap.GetHashTable(commandName);
commandKey.FreeSelf();
if (subCommandToAliasesMap == none) {
return;
}
// Display aliases to command itself first
nextAliasesArray = subCommandToAliasesMap.GetArrayList(T(TEMPTY));
PrintAliasesArray(commandName, none, nextAliasesArray);
_.memory.Free(nextAliasesArray);
// Then aliases to all of its subcommands, in no particular order
iter = subCommandToAliasesMap.Iterate();
while (!iter.HasFinished())
{
nextSubCommand = Text(iter.Getkey()); // cannot be `none`
if (nextSubCommand.IsEmpty())
{
nextSubCommand.FreeSelf();
iter.Next();
continue;
}
nextAliasesArray = ArrayList(iter.Get());
PrintAliasesArray(commandName, nextSubCommand, nextAliasesArray);
_.memory.Free(nextAliasesArray);
_.memory.Free(nextSubCommand);
iter.Next();
}
iter.FreeSelf();
}
private final function PrintAliasesArray(
BaseText commandName,
BaseText subcommandName,
ArrayList aliasesArray)
{
local int i;
local Text nextAlias;
if (commandName == none) return;
if (aliasesArray == none) return;
callerConsole
.Write(T(TALIASES_FOR))
.Write(T(TSPACE))
.UseColorOnce(_.color.TextEmphasis)
.Write(commandName);
if (subcommandName != none)
{
callerConsole
.Write(T(TSPACE))
.UseColorOnce(_.color.TextEmphasis)
.Write(subCommandName);
}
callerConsole.Write(T(TCOLON_SPACE));
for (i = 0; i < aliasesArray.GetLength(); i += 1)
{
if (i > 0) {
callerConsole.Write(T(TCOMMA_SPACE));
}
nextAlias = aliasesArray.GetText(i);
callerConsole.UseColorOnce(_.color.TextNeutral).Write(nextAlias);
nextAlias.FreeSelf();
}
callerConsole.WriteBlock();
}
private final function DisplayCommandHelpPages(ArrayList commandList, bool printedSomething) {
local int i;
local Text nextUserProvidedName;
local MutableText referredSubcommand;
local CommandAPI.CommandConfigInfo nextPair;
// If arguments were empty - at least display our own help page
if (commandList == none) {
nextPair.instance = self;
PrintHelpPageFor(usedName, none, nextPair);
return;
}
// Otherwise - print help for specified commands
for (i = 0; i < commandList.GetLength(); i += 1) {
nextUserProvidedName = commandList.GetText(i);
nextPair = GetCommandFromUserProvidedName(
nextUserProvidedName,
/*out*/ referredSubcommand);
if (nextPair.instance != none && !nextPair.usageForbidden) {
if (printedSomething) {
callerConsole.WriteLine(T(TSEPARATOR));
}
PrintHelpPageFor(
nextUserProvidedName,
referredSubcommand,
nextPair);
printedSomething = true;
} else if (nextPair.instance != none) {
callerConsole.UseColor(_.color.TextFailure);
callerConsole.Write(T(TNO_COMMAND_BEGIN));
callerConsole.Write(nextUserProvidedName);
callerConsole.WriteLine(T(TNO_COMMAND_END));
callerConsole.ResetColor();
}
_.memory.Free(nextPair.instance);
_.memory.Free(nextUserProvidedName);
_.memory.Free(referredSubcommand);
// `referredSubcommand` is passed as an `out` parameter on
// every iteration, so we need to prevent the possibility of its value
// being used.
// NOTE: `nextCommand` and `nextUserProvidedName` are just rewritten.
referredSubcommand = none;
}
}
// Returns `Command` based on the name, given by user.
// `referredSubcommand` is always overwritten (freed if non-`none` value
// is passed) and is used to return name of the subcommand for returned
// `Command` that is specified by `nextUserProvidedName` (only relevant for
// aliases that refer to a particular subcommand).
private final function CommandAPI.CommandConfigInfo GetCommandFromUserProvidedName(
BaseText nextUserProvidedName,
out MutableText referredSubcommand)
{
local CommandAPI.CommandConfigInfo result;
local Text commandAliasValue;
local MutableText parsedCommandName;
// Clear `out` parameter no matter what
if (referredSubcommand != none) {
referredSubcommand.FreeSelf();
referredSubcommand = none;
}
// Try getting command using `nextUserProvidedName` as a literal name
result = _.commands.ResolveCommandForUser(nextUserProvidedName, callerUser);
if (result.instance != none) {
return result;
}
// On failure - try resolving it as an alias
commandAliasValue = _.alias.ResolveCommand(nextUserProvidedName);
ParseCommandNames(commandAliasValue, parsedCommandName, referredSubcommand);
result = _.commands.ResolveCommandForUser(parsedCommandName, callerUser);
if ( result.instance == none
|| !result.instance.IsSubCommandAllowed(referredSubcommand, result.config)) {
_.memory.Free(result.instance);
return result;
}
// Empty subcommand name from the alias is essentially no subcommand name
if (referredSubcommand != none && referredSubcommand.IsEmpty()) {
referredSubcommand.FreeSelf();
referredSubcommand = none;
}
_.memory.Free2(commandAliasValue, parsedCommandName);
return result;
}
private final function PrintHelpPageFor(
BaseText commandAlias,
BaseText referredSubcommand,
CommandAPI.CommandConfigInfo commandPair
) {
local Text commandNameLowerCase, commandNameUpperCase;
// Get capitalized command name
commandNameUpperCase = commandAlias.UpperCopy();
// Print header: name + basic info
callerConsole.UseColor(_.color.textHeader)
.Write(commandNameUpperCase)
.UseColor(_.color.textDefault);
commandNameUpperCase.FreeSelf();
if (commandPair.instance.BorrowData().requiresTarget) {
callerConsole.WriteLine(T(TCMD_WITH_TARGET));
}
else {
callerConsole.WriteLine(T(TCMD_WITHOUT_TARGET));
}
// Print commands and options
commandNameLowerCase = commandAlias.LowerCopy();
PrintCommands(commandPair, commandNameLowerCase, referredSubcommand);
commandNameLowerCase.FreeSelf();
PrintOptions(commandPair.instance.BorrowData());
// Clean up
callerConsole.ResetColor().Flush();
}
private final function PrintCommands(
CommandAPI.CommandConfigInfo commandPair,
BaseText commandName,
BaseText referredSubcommand
) {
local int i;
local array subCommands;
subCommands = commandPair.instance.BorrowData().subCommands;
for (i = 0; i < subCommands.length; i += 1) {
if (referredSubcommand == none || referredSubcommand.Compare(subCommands[i].name)) {
if (commandPair.instance.IsSubCommandAllowed(subCommands[i].name, commandPair.config)) {
PrintSubCommand(subCommands[i], commandName, referredSubcommand != none);
}
}
}
}
private final function PrintSubCommand(
SubCommand subCommand,
BaseText commandName,
bool skipSubcommandName)
{
// Command + parameters
// Command name + sub command name
callerConsole.UseColor(_.color.textEmphasis)
.Write(commandName)
.Write(T(TSPACE));
if ( !skipSubcommandName
&& subCommand.name != none
&& !subCommand.name.IsEmpty())
{
callerConsole.Write(subCommand.name).Write(T(TSPACE));
}
callerConsole.UseColor(_.color.textDefault);
// Parameters
PrintParameters(subCommand.required, subCommand.optional);
callerConsole.Flush();
// Description
if (subCommand.description != none && !subCommand.description.IsEmpty()) {
callerConsole.WriteBlock(subCommand.description);
}
}
private final function PrintOptions(Command.Data data)
{
local int i;
local array