Browse Source

Add command groups support

pull/8/head
Anton Tarasenko 2 years ago
parent
commit
3f45423dc8
  1. 216
      sources/Commands/BuiltInCommands/ACommandHelp.uc
  2. 16
      sources/Commands/Command.uc
  3. 30
      sources/Commands/CommandDataBuilder.uc
  4. 118
      sources/Commands/Commands_Feature.uc

216
sources/Commands/BuiltInCommands/ACommandHelp.uc

@ -28,27 +28,35 @@ 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;
protected function BuildData(CommandDataBuilder builder)
{
builder.Name(P("help"))
.Summary(P("Detailed information about available commands."));
builder.Name(P("help")).Group(P("core"))
.Summary(P("Displays detailed information about available commands."));
builder.OptionalParams()
.ParamTextList(P("commands"))
.Describe(P("Display information about all specified commands."));
.Describe(P("Displays information about all specified commands."));
builder.Option(P("list"))
.Describe(P("Display list of all available commands."));
.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."))
.OptionalParams()
.ParamTextList(P("groups"));
}
protected function Executed(Command.CallData callData, EPlayer callerPlayer)
{
local HashTable parameters, options;;
local ArrayList commandsToDisplay;
local ArrayList commandsToDisplay, commandGroupsToDisplay;
parameters = callData.parameters;
options = callData.options;
// Print command list if "--list" option was specified
if (options.HasKey(P("list"))) {
DisplayCommandList(callerPlayer);
if (options.HasKey(P("list")))
{
commandGroupsToDisplay = options.GetArrayListBy(P("/list/groups"));
DisplayCommandLists(commandGroupsToDisplay);
_.memory.Free(commandGroupsToDisplay);
}
// Help pages.
// Only need to print them if:
@ -58,58 +66,95 @@ protected function Executed(Command.CallData callData, EPlayer callerPlayer)
if (!options.HasKey(P("list")) || parameters.HasKey(P("commands")))
{
commandsToDisplay = parameters.GetArrayList(P("commands"));
DisplayCommandHelpPages(callerPlayer, commandsToDisplay);
DisplayCommandHelpPages(commandsToDisplay);
_.memory.Free(commandsToDisplay);
}
}
private final function DisplayCommandList(EPlayer player)
private final function DisplayCommandLists(ArrayList commandGroupsToDisplay)
{
local int i;
local ConsoleWriter console;
local Command nextCommand;
local Command.Data nextData;
local array<Text> commandNames;
local array<Text> commandNames, groupsNames;
local Commands_Feature commandsFeature;
if (player == none) return;
commandsFeature =
Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
if (commandsFeature == none) return;
console = player.BorrowConsole();
commandNames = commandsFeature.GetCommandNames();
for (i = 0; i < commandNames.length; i += 1)
if (commandsFeature == none) {
return;
}
if (commandGroupsToDisplay == none)
{
nextCommand = commandsFeature.GetCommand(commandNames[i]);
if (nextCommand == none) continue;
groupsNames = commandsFeature.GetGroupsNames();
DisplayCommandsNamesArray(commandsFeature, commandNames);
}
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 = commandsFeature.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]);
}
DisplayCommandsNamesArray(commandsFeature, commandNames);
_.memory.FreeMany(commandNames);
}
}
_.memory.FreeMany(groupsNames);
}
private final function DisplayCommandsNamesArray(
Commands_Feature commandsFeature,
array<Text> commandsNamesArray)
{
local int i;
local Command nextCommand;
local Command.Data nextData;
for (i = 0; i < commandsNamesArray.length; i += 1)
{
nextCommand = commandsFeature.GetCommand(commandsNamesArray[i]);
if (nextCommand == none) {
continue;
}
nextData = nextCommand.BorrowData();
console.UseColor(_.color.textEmphasis)
callerConsole.UseColor(_.color.textEmphasis)
.Write(nextData.name)
.ResetColor()
.Write(T(TCOLUMN_SPACE))
.WriteLine(nextData.summary);
_.memory.Free(nextCommand);
}
_.memory.FreeMany(commandNames);
}
private final function DisplayCommandHelpPages(
EPlayer player,
ArrayList commandList)
private final function DisplayCommandHelpPages(ArrayList commandList)
{
local int i;
local Text nextCommandName;
local Command nextCommand;
local Commands_Feature commandsFeature;
if (player == none) return;
commandsFeature =
Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
if (commandsFeature == none) return;
if (commandsFeature == none) {
return;
}
// If arguments were empty - at least display our own help page
if (commandList == none)
{
PrintHelpPage(player.BorrowConsole(), BorrowData());
PrintHelpPage(BorrowData());
return;
}
// Otherwise - print help for specified commands
@ -118,70 +163,74 @@ private final function DisplayCommandHelpPages(
nextCommandName = commandList.GetText(i);
nextCommand = commandsFeature.GetCommand(nextCommandName);
_.memory.Free(nextCommandName);
if (nextCommand == none) continue;
PrintHelpPage(player.BorrowConsole(), nextCommand.BorrowData());
if (nextCommand == none) {
continue;
}
if (i > 0) {
callerConsole.WriteLine(T(TSEPARATOR));
}
PrintHelpPage(nextCommand.BorrowData());
_.memory.Free(nextCommand);
}
}
// Following methods are mostly self-explanatory,
// all assume that passed `cout != none`
private final function PrintHelpPage(ConsoleWriter cout, Command.Data data)
// Following methods are mostly self-explanatory
private final function PrintHelpPage(Command.Data data)
{
local Text commandNameUpperCase;
// Get capitalized command name
commandNameUpperCase = data.name.UpperCopy();
// Print header: name + basic info
cout.UseColor(_.color.textHeader)
callerConsole.UseColor(_.color.textHeader)
.Write(commandNameUpperCase)
.UseColor(_.color.textDefault);
commandNameUpperCase.FreeSelf();
if (data.requiresTarget) {
cout.WriteLine(T(TCMD_WITH_TARGET));
callerConsole.WriteLine(T(TCMD_WITH_TARGET));
}
else {
cout.WriteLine(T(TCMD_WITHOUT_TARGET));
callerConsole.WriteLine(T(TCMD_WITHOUT_TARGET));
}
// Print commands and options
PrintCommands(cout, data);
PrintOptions(cout, data);
PrintCommands(data);
PrintOptions(data);
// Clean up
cout.ResetColor().Flush();
callerConsole.ResetColor().Flush();
}
private final function PrintCommands(ConsoleWriter cout, Command.Data data)
private final function PrintCommands(Command.Data data)
{
local int i;
local array<SubCommand> subCommands;
subCommands = data.subCommands;
for (i = 0; i < subCommands.length; i += 1) {
PrintSubCommand(cout, subCommands[i], data.name);
PrintSubCommand(subCommands[i], data.name);
}
}
private final function PrintSubCommand(
ConsoleWriter cout,
SubCommand subCommand,
BaseText commandName)
SubCommand subCommand,
BaseText commandName)
{
// Command + parameters
// Command name + sub command name
cout.UseColor(_.color.textEmphasis)
callerConsole.UseColor(_.color.textEmphasis)
.Write(commandName)
.Write(T(TSPACE));
if (subCommand.name != none && !subCommand.name.IsEmpty()) {
cout.Write(subCommand.name).Write(T(TSPACE));
callerConsole.Write(subCommand.name).Write(T(TSPACE));
}
cout.UseColor(_.color.textDefault);
callerConsole.UseColor(_.color.textDefault);
// Parameters
PrintParameters(cout, subCommand.required, subCommand.optional);
cout.Flush();
PrintParameters(subCommand.required, subCommand.optional);
callerConsole.Flush();
// Description
if (subCommand.description != none && !subCommand.description.IsEmpty()) {
cout.WriteBlock(subCommand.description);
callerConsole.WriteBlock(subCommand.description);
}
}
private final function PrintOptions(ConsoleWriter cout, Command.Data data)
private final function PrintOptions(Command.Data data)
{
local int i;
local array<Option> options;
@ -189,22 +238,22 @@ private final function PrintOptions(ConsoleWriter cout, Command.Data data)
if (options.length <= 0) {
return;
}
cout.UseColor(_.color.textSubHeader)
callerConsole
.UseColor(_.color.textSubHeader)
.WriteLine(T(TOPTIONS))
.UseColor(_.color.textDefault);
for (i = 0; i < options.length; i += 1) {
PrintOption(cout, options[i]);
PrintOption(options[i]);
}
}
private final function PrintOption(
ConsoleWriter cout,
Option option)
private final function PrintOption(Option option)
{
local Text shortNameAsText;
// Option short and long names with added key characters
shortNameAsText = _.text.FromCharacter(option.shortName);
cout.UseColor(_.color.textEmphasis)
callerConsole
.UseColor(_.color.textEmphasis)
.Write(T(TKEY)).Write(shortNameAsText) // "-"
.UseColor(_.color.textDefault)
.Write(T(TCOMMA_SPACE)) // ", "
@ -215,18 +264,17 @@ private final function PrintOption(
// Parameters
if (option.required.length != 0 || option.optional.length != 0)
{
cout.Write(T(TSPACE));
PrintParameters(cout, option.required, option.optional);
callerConsole.Write(T(TSPACE));
PrintParameters(option.required, option.optional);
}
cout.Flush();
callerConsole.Flush();
// Description
if (option.description != none && !option.description.IsEmpty()) {
cout.WriteBlock(option.description);
callerConsole.WriteBlock(option.description);
}
}
private final function PrintParameters(
ConsoleWriter cout,
array<Parameter> required,
array<Parameter> optional)
{
@ -234,57 +282,57 @@ private final function PrintParameters(
// Print required
for (i = 0; i < required.length; i += 1)
{
PrintParameter(cout, required[i]);
PrintParameter(required[i]);
if (i < required.length - 1) {
cout.Write(T(TSPACE));
callerConsole.Write(T(TSPACE));
}
}
if (optional.length <= 0) {
return;
}
// Print optional
cout.Write(T(TSPACE)).Write(T(TOPEN_BRACKET));
callerConsole.Write(T(TSPACE)).Write(T(TOPEN_BRACKET));
for (i = 0; i < optional.length; i += 1)
{
PrintParameter(cout, optional[i]);
PrintParameter(optional[i]);
if (i < optional.length - 1) {
cout.Write(T(TSPACE));
callerConsole.Write(T(TSPACE));
}
}
cout.Write(T(TCLOSE_BRACKET));
callerConsole.Write(T(TCLOSE_BRACKET));
}
private final function PrintParameter(ConsoleWriter cout, Parameter parameter)
private final function PrintParameter(Parameter parameter)
{
switch (parameter.type)
{
case CPT_Boolean:
cout.UseColor(_.color.typeBoolean);
callerConsole.UseColor(_.color.typeBoolean);
break;
case CPT_Integer:
cout.UseColor(_.color.typeNumber);
callerConsole.UseColor(_.color.typeNumber);
break;
case CPT_Number:
cout.UseColor(_.color.typeNumber);
callerConsole.UseColor(_.color.typeNumber);
break;
case CPT_Text:
case CPT_Remainder:
cout.UseColor(_.color.typeString);
callerConsole.UseColor(_.color.typeString);
break;
case CPT_Object:
cout.UseColor(_.color.typeLiteral);
callerConsole.UseColor(_.color.typeLiteral);
break;
case CPT_Array:
cout.UseColor(_.color.typeLiteral);
callerConsole.UseColor(_.color.typeLiteral);
break;
default:
cout.UseColor(_.color.textDefault);
callerConsole.UseColor(_.color.textDefault);
}
cout.Write(parameter.displayName);
callerConsole.Write(parameter.displayName);
if (parameter.allowsList) {
cout.Write(T(TPLUS));
callerConsole.Write(T(TPLUS));
}
cout.UseColor(_.color.textDefault);
callerConsole.UseColor(_.color.textDefault);
}
defaultproperties
@ -323,4 +371,10 @@ defaultproperties
stringConstants(15) = ": This command does not require target to be specified."
TOPTIONS = 16
stringConstants(16) = "OPTIONS"
TSEPARATOR = 17
stringConstants(17) = "============================="
TLIST_REGIRESTED_CMDS = 18
stringConstants(18) = "{$TextHeader List of registered commands}"
TEMPTY_GROUP = 19
stringConstants(19) = "Empty group"
}

16
sources/Commands/Command.uc

@ -164,6 +164,8 @@ struct Data
// Default command name that will be used unless Acedia is configured to
// do otherwise
var protected Text name;
// Command group this command belongs to
var protected Text group;
// Short summary of what command does (recommended to
// keep it to 80 characters)
var protected Text summary;
@ -563,6 +565,20 @@ public final function Text GetName()
return commandData.name.LowerCopy();
}
/**
* Returns group name (in lower case) of the caller command class.
*
* @return Group name (in lower case) of the caller command class.
* Guaranteed to be not `none`.
*/
public final function Text GetGroupName()
{
if (commandData.group == none) {
return P("").Copy();
}
return commandData.group.LowerCopy();
}
// TODO: use `SharedRef` instead
/**
* Returns `Command.Data` struct that describes caller `Command`.

30
sources/Commands/CommandDataBuilder.uc

@ -1,7 +1,7 @@
/**
* Utility class that provides developers with a simple interface to
* prepare data that describes command's parameters and options.
* Copyright 2021 Anton Tarasenko
* Copyright 2021-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -42,7 +42,8 @@ class CommandDataBuilder extends AcediaObject
*/
// "Prepared data"
var private Text commandName, commandSummary;
var private Text commandName, commandGroup;
var private Text commandSummary;
var private array<Command.SubCommand> subcommands;
var private array<Command.Option> options;
var private bool requiresTarget;
@ -84,6 +85,7 @@ protected function Finalizer()
optionsIsOptional.length = 0;
selectedParameterArray.length = 0;
commandName = none;
commandGroup = none;
commandSummary = none;
selectedItemName = none;
selectedDescription = none;
@ -474,6 +476,29 @@ public final function CommandDataBuilder Name(BaseText newName)
return self;
}
/**
* Sets new group of `Command.Data` under construction. Group name is meant to
* be shared among several commands, allowing user to filter or fetch commands
* of a certain group.
*
* @return Returns the caller `CommandDataBuilder` to allow for
* method chaining.
*/
public final function CommandDataBuilder Group(BaseText newName)
{
if (newName != none && newName == commandGroup) {
return self;
}
_.memory.Free(commandGroup);
if (newName != none) {
commandGroup = newName.Copy();
}
else {
commandGroup = none;
}
return self;
}
/**
* Sets new summary of `Command.Data` under construction. Summary gives a short
* description of the command on the whole, to be displayed in a command list.
@ -545,6 +570,7 @@ public final function Command.Data BorrowData()
local Command.Data newData;
RecordSelection();
newData.name = commandName;
newData.group = commandGroup;
newData.summary = commandSummary;
newData.subcommands = subcommands;
newData.options = options;

118
sources/Commands/Commands_Feature.uc

@ -27,6 +27,9 @@ var private array<Text> commandDelimiters;
// Registered commands, recorded as (<command_name>, <command_instance>) pairs.
// Keys should be deallocated when their entry is removed.
var private HashTable registeredCommands;
// `HashTable` of "<command_group_name>" <-> `ArrayList` of commands pairs
// to allow quick fetch of commands belonging to a single group
var private HashTable groupedCommands;
// When this flag is set to true, mutate input becomes available
// despite `useMutateInput` flag to allow to unlock server in case of an error
@ -48,16 +51,17 @@ var LoggerAPI.Definition errCommandDuplicate;
protected function OnEnabled()
{
registeredCommands = _.collections.EmptyHashTable();
registeredCommands = _.collections.EmptyHashTable();
groupedCommands = _.collections.EmptyHashTable();
RegisterCommand(class'ACommandHelp');
// Macro selector
commandDelimiters[0] = P("@");
commandDelimiters[0] = _.text.FromString("@");
// Key selector
commandDelimiters[1] = P("#");
commandDelimiters[1] = _.text.FromString("#");
// Player array (possibly JSON array)
commandDelimiters[2] = P("[");
commandDelimiters[2] = _.text.FromString("[");
// Negation of the selector
commandDelimiters[3] = P("!");
commandDelimiters[3] = _.text.FromString("!");
}
protected function OnDisabled()
@ -71,10 +75,13 @@ protected function OnDisabled()
useChatInput = false;
useMutateInput = false;
_.memory.Free(registeredCommands);
registeredCommands = none;
commandDelimiters.length = 0;
_.memory.Free(groupedCommands);
_.memory.Free(chatCommandPrefix);
chatCommandPrefix = none;
_.memory.FreeMany(commandDelimiters);
registeredCommands = none;
groupedCommands = none;
chatCommandPrefix = none;
commandDelimiters.length = 0;
}
protected function SwapConfig(FeatureConfig config)
@ -207,7 +214,8 @@ public final static function Text GetChatPrefix()
*/
public final function RegisterCommand(class<Command> commandClass)
{
local Text commandName;
local Text commandName, groupName;
local ArrayList groupArray;
local Command newCommandInstance, existingCommandInstance;
if (commandClass == none) return;
@ -215,6 +223,7 @@ public final function RegisterCommand(class<Command> commandClass)
newCommandInstance = Command(_.memory.Allocate(commandClass, true));
commandName = newCommandInstance.GetName();
groupName = newCommandInstance.GetGroupName();
// Check for duplicates and report them
existingCommandInstance = Command(registeredCommands.GetItem(commandName));
if (existingCommandInstance != none)
@ -223,6 +232,7 @@ public final function RegisterCommand(class<Command> commandClass)
.ArgClass(existingCommandInstance.class)
.Arg(commandName)
.ArgClass(commandClass);
_.memory.Free(groupName);
_.memory.Free(newCommandInstance);
_.memory.Free(existingCommandInstance);
return;
@ -230,6 +240,16 @@ public final function RegisterCommand(class<Command> commandClass)
// Otherwise record new command
// `commandName` used as a key, do not deallocate it
registeredCommands.SetItem(commandName, newCommandInstance);
// Add to grouped collection
groupArray = groupedCommands.GetArrayList(groupName);
if (groupArray == none) {
groupArray = _.collections.EmptyArrayList();
}
groupArray.AddItem(newCommandInstance);
groupedCommands.SetItem(groupName, groupArray);
_.memory.Free(groupArray);
_.memory.Free(groupName);
_.memory.Free(commandName);
_.memory.Free(newCommandInstance);
}
@ -249,6 +269,7 @@ public final function RemoveCommand(class<Command> commandClass)
local Iter iter;
local Command nextCommand;
local Text nextCommandName;
local array<Text> commandGroup;
local array<Text> keysToRemove;
if (commandClass == none) return;
@ -266,6 +287,7 @@ public final function RemoveCommand(class<Command> commandClass)
continue;
}
keysToRemove[keysToRemove.length] = nextCommandName;
commandGroup[commandGroup.length] = nextCommand.GetGroupName();
_.memory.Free(nextCommand);
}
iter.FreeSelf();
@ -274,6 +296,40 @@ public final function RemoveCommand(class<Command> commandClass)
registeredCommands.RemoveItem(keysToRemove[i]);
_.memory.Free(keysToRemove[i]);
}
for (i = 0; i < commandGroup.length; i += 1) {
RemoveClassFromGroup(commandClass, commandGroup[i]);
}
_.memory.FreeMany(commandGroup);
}
private final function RemoveClassFromGroup(
class<Command> commandClass,
BaseText commandGroup)
{
local int i;
local ArrayList groupArray;
local Command nextCommand;
groupArray = groupedCommands.GetArrayList(commandGroup);
if (groupArray == none) {
return;
}
while (i < groupArray.GetLength())
{
nextCommand = Command(groupArray.GetItem(i));
if (nextCommand != none && nextCommand.class == commandClass) {
groupArray.RemoveIndex(i);
}
else {
i += 1;
}
_.memory.Free(nextCommand);
}
if (groupArray.GetLength() == 0) {
groupedCommands.RemoveItem(commandGroup);
}
_.memory.Free(groupArray);
}
/**
@ -313,6 +369,50 @@ public final function array<Text> GetCommandNames()
return emptyResult;
}
/**
* Returns array of names of all available commands belonging to the group
* `groupName`.
*
* @return Array of names of all available (registered) commands, belonging to
* the group `groupName`.
*/
public final function array<Text> GetCommandNamesInGroup(BaseText groupName)
{
local int i;
local ArrayList groupArray;
local Command nextCommand;
local array<Text> result;
if (groupedCommands == none) return result;
groupArray = groupedCommands.GetArrayList(groupName);
if (groupArray == none) return result;
for (i = 0; i < groupArray.GetLength(); i += 1)
{
nextCommand = Command(groupArray.GetItem(i));
if (nextCommand != none) {
result[result.length] = nextCommand.GetName();
}
_.memory.Free(nextCommand);
}
return result;
}
/**
* Returns all available command groups' names.
*
* @return Array of all available command groups' names.
*/
public final function array<Text> GetGroupsNames()
{
local array<Text> emptyResult;
if (groupedCommands != none) {
return groupedCommands.GetTextKeys();
}
return emptyResult;
}
/**
* Handles user input: finds appropriate command and passes the rest of
* the arguments to it for further processing.

Loading…
Cancel
Save