/** * 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