From 626124335d79afbe996003a9e440e3bfc1bcd3c6 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 13 Mar 2023 04:04:08 +0700 Subject: [PATCH 01/17] Fix style for `CommandDataBuilder` --- .../Commands/BuiltInCommands/ACommandHelp.uc | 31 +- .../Features/Commands/CommandDataBuilder.uc | 1467 +++++++---------- .../Features/Commands/Tests/MockCommandA.uc | 21 +- .../Features/Commands/Tests/MockCommandB.uc | 58 +- .../Commands/Tests/TEST_CommandDataBuilder.uc | 24 +- sources/Users/ACommandUserGroups.uc | 103 +- 6 files changed, 722 insertions(+), 982 deletions(-) diff --git a/sources/LevelAPI/Features/Commands/BuiltInCommands/ACommandHelp.uc b/sources/LevelAPI/Features/Commands/BuiltInCommands/ACommandHelp.uc index a4ec0f9..0540cc7 100644 --- a/sources/LevelAPI/Features/Commands/BuiltInCommands/ACommandHelp.uc +++ b/sources/LevelAPI/Features/Commands/BuiltInCommands/ACommandHelp.uc @@ -102,21 +102,22 @@ protected function Finalizer() protected function BuildData(CommandDataBuilder builder) { - builder.Name(P("help")).Group(P("core")) - .Summary(P("Displays detailed information about available commands.")); - builder.OptionalParams() - .ParamTextList(P("commands")) - .Describe(P("Displays information about all specified commands.")); - builder.Option(P("aliases")) - .Describe(P("When displaying available commands, specifying this flag" - @ "will additionally make command to display all of their available" - @ "aliases.")) - .Option(P("list")) - .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")); + builder.Name(P("help")); + 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) diff --git a/sources/LevelAPI/Features/Commands/CommandDataBuilder.uc b/sources/LevelAPI/Features/Commands/CommandDataBuilder.uc index c441157..8232917 100644 --- a/sources/LevelAPI/Features/Commands/CommandDataBuilder.uc +++ b/sources/LevelAPI/Features/Commands/CommandDataBuilder.uc @@ -1,7 +1,8 @@ /** - * Utility class that provides developers with a simple interface to - * prepare data that describes command's parameters and options. - * Copyright 2021-2022 Anton Tarasenko + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -21,105 +22,82 @@ class CommandDataBuilder extends AcediaObject dependson(Command); -/** - * # `CommandDataBuilder` - * - * This class is made for convenient creation of `Command.Data` using - * a builder pattern. - * - * ## Usage - * - * `CommandDataBuilder` should be able to fill information about: - * subcommands/options and their parameters. - * As far as user is concerned, the process of filling both should be - * identical. Overall, intended flow for creating a new sub-command or option - * is to select either, fill it with data with public methods `Param...()` into - * "selected data" and then copy it into "prepared data" - * (through a `RecordSelection()` method below). - * For examples see `BuildData()` methods from `ACommandHelp` or any of - * the commands from "Futility" package. - * - * ## Implementation - * - * We will store all defined data in two ways: - * - * 1. Selected data: data about parameters for subcommand/option that is - * currently being filled; - * 2. Prepared data: data that was already filled as "selected data" then - * stored in these records. Whenever we want to switch to filling - * another subcommand/option or return already prepared data we must - * dump "selected data" into "prepared data" first and then return - * the latter. - * - * Builder object is automatically created when new `Command` instance is - * allocated and, therefore, doesn't normally need to be allocated by hand. - */ - -// "Prepared data" -var private Text commandName, commandGroup; -var private Text commandSummary; -var private array subcommands; -var private array options; -var private bool requiresTarget; -// Auxiliary arrays signifying that we've started adding optional -// parameters into appropriate `subcommands` and `options`. -// All optional parameters must follow strictly after required parameters -// and so, after user have started adding optional parameters to -// subcommand/option, we prevent them from adding required ones -// (to that particular command/option). +//! This is an auxiliary class for convenient creation of [`Command::Data`] using a builder pattern. +//! +//! ## Implementation +//! +//! We will store all defined data in two ways: +//! +//! 1. Selected data: data about parameters for subcommand/option that is currently being filled; +//! 2. Prepared data: data that was already filled as "selected data" then stored in these records. +//! Whenever we want to switch to filling another subcommand/option or return already prepared +//! data we must dump "selected data" into "prepared data" first and then return the latter. +//! +//! Builder object is automatically created when new `Command` instance is allocated and doesn't +//! normally need to be allocated by hand. + +// "Prepared data" +var private Text commandName, commandGroup; +var private Text commandSummary; +var private array subcommands; +var private array options; +var private bool requiresTarget; + +// Auxiliary arrays signifying that we've started adding optional parameters into appropriate +// `subcommands` and `options`. +// +// All optional parameters must follow strictly after required parameters and so, after user have +// started adding optional parameters to subcommand/option, we prevent them from adding required +// ones (to that particular command/option). var private array subcommandsIsOptional; var private array optionsIsOptional; -// "Selected data" -// `false` means we have selected sub-command, `true` - option -var private bool selectedItemIsOption; -// `name` for sub-commands, `longName` for options -var private Text selectedItemName; -// Description of selected sub-command/option -var private Text selectedDescription; -// Are we filling optional parameters (`true`)? Or required ones (`false`)? -var private bool selectionIsOptional; -// Array of parameters we are currently filling (either required or optional) -var private array selectedParameterArray; - -var LoggerAPI.Definition errLongNameTooShort, errShortNameTooLong; -var LoggerAPI.Definition warnSameLongName, warnSameShortName; - -protected function Constructor() -{ +// "Selected data" +// `false` means we have selected sub-command, `true` - option +var private bool selectedItemIsOption; +// `name` for sub-commands, `longName` for options +var private Text selectedItemName; +// Description of selected sub-command/option +var private Text selectedDescription; +// Are we filling optional parameters (`true`)? Or required ones (`false`)? +var private bool selectionIsOptional; +// Array of parameters we are currently filling (either required or optional) +var private array selectedParameterArray; + +var private LoggerAPI.Definition errLongNameTooShort, errShortNameTooLong; +var private LoggerAPI.Definition warnSameLongName, warnSameShortName; + +protected function Constructor() { // Fill empty subcommand (no special key word) by default - SelectSubCommand(P("")); + SubCommand(P("")); } -protected function Finalizer() -{ - subcommands.length = 0; - subcommandsIsOptional.length = 0; - options.length = 0; - optionsIsOptional.length = 0; - selectedParameterArray.length = 0; - commandName = none; - commandGroup = none; - commandSummary = none; - selectedItemName = none; - selectedDescription = none; - requiresTarget = false; - selectedItemIsOption = false; - selectionIsOptional = false; -} - -// Find index of sub-command with a given name `name` in `subcommands`. -// `-1` if there's not sub-command with such name. -// Case-sensitive. -private final function int FindSubCommandIndex(BaseText name) -{ +protected function Finalizer() { + subcommands.length = 0; + subcommandsIsOptional.length = 0; + options.length = 0; + optionsIsOptional.length = 0; + selectedParameterArray.length = 0; + commandName = none; + commandGroup = none; + commandSummary = none; + selectedItemName = none; + selectedDescription = none; + requiresTarget = false; + selectedItemIsOption = false; + selectionIsOptional = false; +} + +// Find index of sub-command with a given name `name` in `subcommands`. +// `-1` if there's not sub-command with such name. +// Case-sensitive. +private final function int FindSubCommandIndex(BaseText name) { local int i; if (name == none) { return -1; } - for (i = 0; i < subcommands.length; i += 1) - { + for (i = 0; i < subcommands.length; i += 1) { if (name.Compare(subcommands[i].name)) { return i; } @@ -127,18 +105,16 @@ private final function int FindSubCommandIndex(BaseText name) return -1; } -// Find index of option with a given name `name` in `options`. -// `-1` if there's not sub-command with such name. -// Case-sensitive. -private final function int FindOptionIndex(BaseText longName) -{ +// Find index of option with a given name `name` in `options`. +// `-1` if there's not sub-command with such name. +// Case-sensitive. +private final function int FindOptionIndex(BaseText longName) { local int i; if (longName == none) { return -1; } - for (i = 0; i < options.length; i += 1) - { + for (i = 0; i < options.length; i += 1) { if (longName.Compare(options[i].longName)) { return i; } @@ -146,180 +122,111 @@ private final function int FindOptionIndex(BaseText longName) return -1; } -// Creates an empty selection record for subcommand or option with -// name (long name) `name`. -// Doe not check whether subcommand/option with that name already exists. -// Copies passed `name`, assumes that it is not `none`. -private final function MakeEmptySelection(BaseText name, bool selectedOption) -{ - selectedItemIsOption = selectedOption; - selectedItemName = name.Copy(); - selectedDescription = none; - selectedParameterArray.length = 0; - selectionIsOptional = false; +// Creates an empty selection record for subcommand or option with name (long name) `name`. +// Doe not check whether subcommand/option with that name already exists. +// Copies passed `name`, assumes that it is not `none`. +private final function MakeEmptySelection(BaseText name, bool selectedOption) { + selectedItemIsOption = selectedOption; + selectedItemName = name.Copy(); + selectedDescription = none; + selectedParameterArray.length = 0; + selectionIsOptional = false; } -// Select sub-command with a given name `name` from `subcommands`. -// If there is no command with specified name `name` in prepared data - -// creates new record in selection, otherwise copies previously saved data. -// Automatically saves previously selected data into prepared data. -// Copies `name` if it has to create new record. -private final function SelectSubCommand(BaseText name) -{ - local int subcommandIndex; +// Select option with a given long name `longName` from `options`. +// If there is no option with specified `longName` in prepared data - creates new record in +// selection, otherwise copies previously saved data. +// Automatically saves previously selected data into prepared data. +// Copies `name` if it has to create new record. +private final function SelectOption(BaseText longName) { + local int optionIndex; - if (name == none) return; - if ( !selectedItemIsOption && selectedItemName != none - && selectedItemName.Compare(name)) - { - return; - } - RecordSelection(); - subcommandIndex = FindSubCommandIndex(name); - if (subcommandIndex < 0) - { - MakeEmptySelection(name, false); + if (longName == none) { return; } - // Load appropriate prepared data, if it exists for - // sub-command with name `name` - selectedItemIsOption = false; - selectedItemName = subcommands[subcommandIndex].name; - selectedDescription = subcommands[subcommandIndex].description; - selectionIsOptional = subcommandsIsOptional[subcommandIndex] > 0; - if (selectionIsOptional) { - selectedParameterArray = subcommands[subcommandIndex].optional; - } - else { - selectedParameterArray = subcommands[subcommandIndex].required; - } -} - -// Select option with a given long name `longName` from `options`. -// If there is no option with specified `longName` in prepared data - -// creates new record in selection, otherwise copies previously saved data. -// Automatically saves previously selected data into prepared data. -// Copies `name` if it has to create new record. -private final function SelectOption(BaseText longName) -{ - local int optionIndex; - - if (longName == none) return; - if ( selectedItemIsOption && selectedItemName != none - && selectedItemName.Compare(longName)) - { + if (selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(longName)) { return; } RecordSelection(); optionIndex = FindOptionIndex(longName); - if (optionIndex < 0) - { + if (optionIndex < 0) { MakeEmptySelection(longName, true); return; } - // Load appropriate prepared data, if it exists for - // option with long name `longName` - selectedItemIsOption = true; - selectedItemName = options[optionIndex].longName; - selectedDescription = options[optionIndex].description; - selectionIsOptional = optionsIsOptional[optionIndex] > 0; + // Load appropriate prepared data, if it exists for + // option with long name `longName` + selectedItemIsOption = true; + selectedItemName = options[optionIndex].longName; + selectedDescription = options[optionIndex].description; + selectionIsOptional = optionsIsOptional[optionIndex] > 0; if (selectionIsOptional) { selectedParameterArray = options[optionIndex].optional; - } - else { + } else { selectedParameterArray = options[optionIndex].required; } } // Saves currently selected data into prepared data. -private final function RecordSelection() -{ +private final function RecordSelection() { if (selectedItemName == none) { return; } if (selectedItemIsOption) { RecordSelectedOption(); - } - else { + } else { RecordSelectedSubCommand(); } } // Saves selected sub-command into prepared records. // Assumes that command and not an option is selected. -private final function RecordSelectedSubCommand() -{ - local int selectedSubCommandIndex; - local Command.SubCommand newSubcommand; - - if (selectedItemName == none) return; +private final function RecordSelectedSubCommand() { + local int selectedSubCommandIndex; + local Command.SubCommand newSubcommand; + if (selectedItemName == none) { + return; + } selectedSubCommandIndex = FindSubCommandIndex(selectedItemName); - if (selectedSubCommandIndex < 0) - { + if (selectedSubCommandIndex < 0) { selectedSubCommandIndex = subcommands.length; subcommands[selectedSubCommandIndex] = newSubcommand; } - subcommands[selectedSubCommandIndex].name = selectedItemName; - subcommands[selectedSubCommandIndex].description = selectedDescription; - if (selectionIsOptional) - { + subcommands[selectedSubCommandIndex].name = selectedItemName; + subcommands[selectedSubCommandIndex].description = selectedDescription; + if (selectionIsOptional) { subcommands[selectedSubCommandIndex].optional = selectedParameterArray; subcommandsIsOptional[selectedSubCommandIndex] = 1; - } - else - { + } else { subcommands[selectedSubCommandIndex].required = selectedParameterArray; subcommandsIsOptional[selectedSubCommandIndex] = 0; } } -// Saves currently selected option into prepared records. -// Assumes that option and not an command is selected. -private final function RecordSelectedOption() -{ - local int selectedOptionIndex; - local Command.Option newOption; - - if (selectedItemName == none) return; +// Saves currently selected option into prepared records. +// Assumes that option and not an command is selected. +private final function RecordSelectedOption() { + local int selectedOptionIndex; + local Command.Option newOption; + if (selectedItemName == none) { + return; + } selectedOptionIndex = FindOptionIndex(selectedItemName); - if (selectedOptionIndex < 0) - { + if (selectedOptionIndex < 0) { selectedOptionIndex = options.length; options[selectedOptionIndex] = newOption; } - options[selectedOptionIndex].longName = selectedItemName; - options[selectedOptionIndex].description = selectedDescription; - if (selectionIsOptional) - { + options[selectedOptionIndex].longName = selectedItemName; + options[selectedOptionIndex].description = selectedDescription; + if (selectionIsOptional) { options[selectedOptionIndex].optional = selectedParameterArray; optionsIsOptional[selectedOptionIndex] = 1; - } - else - { + } else { options[selectedOptionIndex].required = selectedParameterArray; optionsIsOptional[selectedOptionIndex] = 0; } } -/** - * Method to use to start defining a new sub-command. - * - * Does two things: - * 1. Creates new sub-command with a given name (if it's missing); - * 2. Selects sub-command with name `name` to add parameters to. - * - * @param name Name of the sub-command user wants to define, - * case-sensitive. Variable will be copied. - * If `none` is passed, this method will do nothing. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder SubCommand(BaseText name) -{ - SelectSubCommand(name); - return self; -} // Validates names (printing errors in case of failure) for the option. // Long name must be at least 2 characters long. @@ -333,14 +240,13 @@ public final function CommandDataBuilder SubCommand(BaseText name) // (if `shortName` was used for it - it's value will be copied). private final function BaseText.Character GetValidShortName( BaseText longName, - BaseText shortName) -{ + BaseText shortName +) { // Validate `longName` if (longName == none) { return _.text.GetInvalidCharacter(); } - if (longName.GetLength() < 2) - { + if (longName.GetLength() < 2) { _.logger.Auto(errLongNameTooShort).ArgClass(class).Arg(longName.Copy()); return _.text.GetInvalidCharacter(); } @@ -349,8 +255,7 @@ private final function BaseText.Character GetValidShortName( if (shortName == none) { return longName.GetCharacter(0); } - if (shortName.IsEmpty() || shortName.GetLength() > 1) - { + if (shortName.IsEmpty() || shortName.GetLength() > 1) { _.logger.Auto(errShortNameTooLong).ArgClass(class).Arg(longName.Copy()); return _.text.GetInvalidCharacter(); } @@ -363,780 +268,596 @@ private final function BaseText.Character GetValidShortName( // i.e. we cannot have several options with identical names: // (--silent, -s) and (--sick, -s). private final function bool VerifyNoOptionNamingConflict( - BaseText longName, - BaseText.Character shortName) -{ + BaseText longName, + BaseText.Character shortName +) { local int i; + local bool sameShortNames, sameLongNames; // To make sure we will search through the up-to-date `options`, // record selection into prepared records. RecordSelection(); - for (i = 0; i < options.length; i += 1) - { - // Is same long name, but different long names? - if ( !_.text.AreEqual(shortName, options[i].shortName) - && longName.Compare(options[i].longName)) - { - _.logger.Auto(warnSameLongName) - .ArgClass(class) - .Arg(longName.Copy()); + for (i = 0; i < options.length; i += 1) { + sameShortNames = _.text.AreEqual(shortName, options[i].shortName); + sameLongNames = longName.Compare(options[i].longName); + if (sameLongNames && !sameShortNames) { + _.logger.Auto(warnSameLongName).ArgClass(class).Arg(longName.Copy()); return true; } - // Is same short name, but different short ones? - if ( _.text.AreEqual(shortName, options[i].shortName) - && !longName.Compare(options[i].longName)) - { - _.logger.Auto(warnSameLongName) - .ArgClass(class) - .Arg(_.text.FromCharacter(shortName)); + if (!sameLongNames && sameShortNames) { + _.logger.Auto(warnSameLongName).ArgClass(class).Arg(_.text.FromCharacter(shortName)); return true; } } return false; } -/** - * Method to use to start defining a new option. - * - * Does three things: - * 1. Checks if some of the recorded options are in conflict with given - * `longName` and `shortName` (already using one and only one of them). - * 2. Creates new option with a long and short names - * (if such option is missing); - * 3. Selects option with a long name `longName` to add parameters to. - * - * @param longName Long name of the option, case-sensitive - * (for using an option in form "--..."). - * Must be at least two characters long. If passed value is either `none` - * or too short, method will log an error and omits this option. - * @param shortName Short name of the option, case-sensitive - * (for using an option in form "-..."). - * Must be exactly one character. If `none` value is passed - * (or the argument altogether omitted) - uses first character of - * the `longName`. - * If `shortName` is not `none` and is not exactly 1 character long - - * logs an error and omits this option. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder Option( - BaseText longName, - optional BaseText shortName) -{ +/// Method that starts defining a new sub-command. +/// +/// Creates new sub-command with a given name (if it's missing) and then selects sub-command with +/// a given name to add parameters to. +/// +/// [`name`] defines name of the sub-command user wants, case-sensitive. +/// If `none` is passed, this method will do nothing. +public final function SubCommand(BaseText name) { + local int subcommandIndex; + + if (name == none) { + return; + } + if (!selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(name)) { + return; + } + RecordSelection(); + subcommandIndex = FindSubCommandIndex(name); + if (subcommandIndex < 0) { + MakeEmptySelection(name, false); + return; + } + // Load appropriate prepared data, if it exists for + // sub-command with name `name` + selectedItemIsOption = false; + selectedItemName = subcommands[subcommandIndex].name; + selectedDescription = subcommands[subcommandIndex].description; + selectionIsOptional = subcommandsIsOptional[subcommandIndex] > 0; + if (selectionIsOptional) { + selectedParameterArray = subcommands[subcommandIndex].optional; + } else { + selectedParameterArray = subcommands[subcommandIndex].required; + } +} + +/// Method that starts defining a new option. +/// +/// This method checks if some of the recorded options are in conflict with given `longName` and +/// `shortName` (already using one and only one of them). +/// In case there is no conflict, it creates new option with specified long and short names +/// (if such option is missing) and selects option with a long name `longName` to add parameters to. +/// +/// [`longName`] defines long name of the option, case-sensitive (for using an option in form +/// "--..."). Must be at least two characters long. +/// [`shortName`] defines short name of the option, case-sensitive (for using an option in form +/// "-..."). Must be exactly one character. +/// +/// # Errors +/// +/// Errors will be logged in case either of arguments are `none`, have inappropriate length or are +/// in conflict with each other. +public final function Option(BaseText longName, optional BaseText shortName) { local int optionIndex; local BaseText.Character shortNameAsCharacter; - // Unlike for `SubCommand()`, we need to ensure that option naming is - // correct and does not conflict with existing options - // (user might attempt to add two options with same long names and - // different short ones). + // Unlike for `SubCommand()`, we need to ensure that option naming is + // correct and does not conflict with existing options + // (user might attempt to add two options with same long names and + // different short ones). shortNameAsCharacter = GetValidShortName(longName, shortName); if ( !_.text.IsValidCharacter(shortNameAsCharacter) - || VerifyNoOptionNamingConflict(longName, shortNameAsCharacter)) - { + || VerifyNoOptionNamingConflict(longName, shortNameAsCharacter)) { // ^ `GetValidShortName()` and `VerifyNoOptionNamingConflict()` // are responsible for logging warnings/errors - return self; + return; } SelectOption(longName); // Set short name for new options optionIndex = FindOptionIndex(longName); - if (optionIndex < 0) - { + if (optionIndex < 0) { // We can only be here if option was created for the first time RecordSelection(); // So now it cannot fail optionIndex = FindOptionIndex(longName); options[optionIndex].shortName = shortNameAsCharacter; } - return self; } -/** - * Adds description to the selected sub-command / option. - * - * Previous description is discarded (default description is empty). - * - * Does nothing if nothing is selected. - * - * @param description New description of selected sub-command / option. - * Variable will be copied. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder Describe(BaseText description) -{ - if (selectedDescription == description) { - return self; - } +/// Adds description to the selected sub-command / option. +/// +/// Does nothing if nothing is yet selected. +public final function Describe(BaseText description) { _.memory.Free(selectedDescription); if (description != none) { selectedDescription = description.Copy(); } - return self; } -/** - * Sets new name of `Command.Data` under construction. This is a name that will - * be used unless Acedia is configured to do otherwise. - * - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder Name(BaseText newName) -{ +/// Sets new name of the command that caller [`CommandDataBuilder`] constructs. +public final function Name(BaseText newName) { if (newName != none && newName == commandName) { - return self; + return; } _.memory.Free(commandName); if (newName != none) { commandName = newName.Copy(); - } - else { + } else { commandName = none; } - 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) -{ +/// 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. +public final function Group(BaseText newName) { if (newName != none && newName == commandGroup) { - return self; + return; } _.memory.Free(commandGroup); if (newName != none) { commandGroup = newName.Copy(); - } - else { + } 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. - * - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder Summary(BaseText newSummary) -{ +/// Sets new summary of `Command.Data` under construction. +/// +/// Summary gives a short description of the command on the whole that will displayed when "help" +/// command is listing available command +public final function Summary(BaseText newSummary) { if (newSummary != none && newSummary == commandSummary) { - return self; + return; } _.memory.Free(commandSummary); if (newSummary != none) { commandSummary = newSummary.Copy(); - } - else { + } else { commandSummary = none; } - return self; } -/** - * Makes caller builder to mark `Command.Data` under construction to require - * a player target. - * - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder RequireTarget() -{ +/// Makes caller builder to mark `Command.Data` under construction to require a player target. +public final function RequireTarget() { requiresTarget = true; - return self; } -/** - * Any parameters added to currently selected sub-command / option after - * calling this method will be marked as optional. - * - * Further calls when the same sub-command / option is selected do nothing. - * - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder OptionalParams() + +/// Any parameters added to currently selected sub-command / option after calling this method will +/// be marked as optional. +/// +/// Further calls when the same sub-command / option is selected will do nothing. +public final function OptionalParams() { if (selectionIsOptional) { - return self; + return; } // Record all required parameters first, otherwise there would be no way // to distinguish between them and optional parameters RecordSelection(); selectionIsOptional = true; selectedParameterArray.length = 0; - return self; } -/** - * Returns data that has been constructed so far by - * the caller `CommandDataBuilder`. - * - * Does not reset progress. - * - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function Command.Data BorrowData() -{ +/// Returns data that has been constructed so far by the caller [`CommandDataBuilder`]. +/// +/// Does not reset progress. +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; - newData.requiresTarget = requiresTarget; + newData.name = commandName; + newData.group = commandGroup; + newData.summary = commandSummary; + newData.subcommands = subcommands; + newData.options = options; + newData.requiresTarget = requiresTarget; return newData; } -// Adds new parameter to selected sub-command / option -private final function PushParameter(Command.Parameter newParameter) -{ +// Adds new parameter to selected sub-command / option +private final function PushParameter(Command.Parameter newParameter) { selectedParameterArray[selectedParameterArray.length] = newParameter; } -// Fills `Command.ParameterType` struct with given values -// (except boolean format). Assumes `displayName != none`. +// Fills `Command.ParameterType` struct with given values (except boolean format). +// Assumes `displayName != none`. private final function Command.Parameter NewParameter( - BaseText displayName, - Command.ParameterType parameterType, - bool isListParameter, - optional BaseText variableName) -{ + BaseText displayName, + Command.ParameterType parameterType, + bool isListParameter, + optional BaseText variableName +) { local Command.Parameter newParameter; - newParameter.displayName = displayName.Copy(); - newParameter.type = parameterType; - newParameter.allowsList = isListParameter; + newParameter.displayName = displayName.Copy(); + newParameter.type = parameterType; + newParameter.allowsList = isListParameter; if (variableName != none) { newParameter.variableName = variableName.Copy(); - } - else { + } else { newParameter.variableName = displayName.Copy(); } return newParameter; } -/** - * Adds new boolean parameter (required or optional depends on whether - * `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * - * @param format Preferred format of boolean values. - * Command parser will still accept boolean values in any form, - * this setting only affects how parameter will be displayed in - * generated help. - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamBoolean( - BaseText name, +/// Adds new boolean parameter (required or optional depends on whether `OptionalParams()` call +/// happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`format`] defines preferred format of boolean values. +/// Command parser will still accept boolean values in any form, this setting only affects how +/// parameter will be displayed in generated help. +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamBoolean( + BaseText name, optional Command.PreferredBooleanFormat format, - optional BaseText variableName) -{ + optional BaseText variableName +) { local Command.Parameter newParam; - if (name == none) { - return self; - } - newParam = NewParameter(name, CPT_Boolean, false, variableName); - newParam.booleanFormat = format; - PushParameter(newParam); - return self; -} - -/** - * Adds new boolean list parameter (required or optional depends on whether - * `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param format Preferred format of boolean values. - * Command parser will still accept boolean values in any form, - * this setting only affects how parameter will be displayed in - * generated help. - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamBooleanList( - BaseText name, + if (name != none) { + newParam = NewParameter(name, CPT_Boolean, false, variableName); + newParam.booleanFormat = format; + PushParameter(newParam); + } +} + +/// Adds new integer list parameter (required or optional depends on whether `OptionalParams()` +/// call happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// List parameters expect user to enter one or more value of the same type as command's arguments. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`format`] defines preferred format of boolean values. +/// Command parser will still accept boolean values in any form, this setting only affects how +/// parameter will be displayed in generated help. +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamBooleanList( + BaseText name, optional Command.PreferredBooleanFormat format, - optional BaseText variableName) -{ + optional BaseText variableName +) { local Command.Parameter newParam; - if (name == none) { - return self; - } - newParam = NewParameter(name, CPT_Boolean, true, variableName); - newParam.booleanFormat = format; - PushParameter(newParam); - return self; -} - -/** - * Adds new integer parameter (required or optional depends on whether - * `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamInteger( - BaseText name, - optional BaseText variableName) -{ - if (name == none) { - return self; - } - PushParameter(NewParameter(name, CPT_Integer, false, variableName)); - return self; -} - -/** - * Adds new integer list parameter (required or optional depends on whether - * `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamIntegerList( - BaseText name, - optional BaseText variableName) -{ - if (name == none) { - return self; - } - PushParameter(NewParameter(name, CPT_Integer, true, variableName)); - return self; -} - -/** - * Adds new numeric parameter (required or optional depends on whether - * `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamNumber( - BaseText name, - optional BaseText variableName) -{ - if (name == none) { - return self; - } - PushParameter(NewParameter(name, CPT_Number, false, variableName)); - return self; -} - -/** - * Adds new numeric list parameter (required or optional depends on whether - * `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamNumberList( - BaseText name, - optional BaseText variableName) -{ - if (name == none) { - return self; - } - PushParameter(NewParameter(name, CPT_Number, true, variableName)); - return self; -} - -/** - * Adds new text parameter (required or optional depends on whether - * `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @param aliasSourceName Name of the alias source that must be used to - * auto-resolve this parameter's value. `none` means that parameter will be - * recorded as-is, any other value (either "weapon", "color", "feature", - * "entity" or some kind of custom alias source name) will make values - * prefixed with "$" to be resolved as aliases. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamText( - BaseText name, - optional BaseText variableName, - optional BaseText aliasSourceName) -{ + if (name != none) { + newParam = NewParameter(name, CPT_Boolean, true, variableName); + newParam.booleanFormat = format; + PushParameter(newParam); + } +} + +/// Adds new integer parameter (required or optional depends on whether `OptionalParams()` call +/// happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamInteger(BaseText name, optional BaseText variableName) { + if (name != none) { + PushParameter(NewParameter(name, CPT_Integer, false, variableName)); + } +} + +/// Adds new integer list parameter (required or optional depends on whether `OptionalParams()` +/// call happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// List parameters expect user to enter one or more value of the same type as command's arguments. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamIntegerList(BaseText name, optional BaseText variableName) { + if (name != none) { + PushParameter(NewParameter(name, CPT_Integer, true, variableName)); + } +} + +/// Adds new numeric parameter (required or optional depends on whether `OptionalParams()` call +/// happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamNumber(BaseText name, optional BaseText variableName) { + if (name != none) { + PushParameter(NewParameter(name, CPT_Number, false, variableName)); + } +} + +/// Adds new numeric list parameter (required or optional depends on whether `OptionalParams()` +/// call happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// List parameters expect user to enter one or more value of the same type as command's arguments. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamNumberList(BaseText name, optional BaseText variableName) { + if (name != none) { + PushParameter(NewParameter(name, CPT_Number, true, variableName)); + } +} + +/// Adds new text parameter (required or optional depends on whether `OptionalParams()` call +/// happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +/// +/// [`aliasSourceName`] defines name of the alias source that must be used to auto-resolve this +/// parameter's value. `none` means that parameter will be recorded as-is, any other value +/// (either "weapon", "color", "feature", "entity" or some kind of custom alias source name) will +/// make values prefixed with "$" to be resolved as custom aliases. +public final function ParamText( + BaseText name, + optional BaseText variableName, + optional BaseText aliasSourceName +) { local Command.Parameter newParameterValue; if (name == none) { - return self; + return; } newParameterValue = NewParameter(name, CPT_Text, false, variableName); if (aliasSourceName != none) { newParameterValue.aliasSourceName = aliasSourceName.Copy(); } PushParameter(newParameterValue); - return self; } -/** - * Adds new text list parameter (required or optional depends on whether - * `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @param aliasSource Name of the alias source that must be used to - * auto-resolve this parameter's value. `none` means that parameter will be - * recorded as-is, any other value (either "weapon", "color", "feature", - * "entity" or some kind of custom alias source name) will make values - * prefixed with "$" to be resolved as aliases. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamTextList( - BaseText name, - optional BaseText variableName, - optional BaseText aliasSourceName) -{ +/// Adds new text list parameter (required or optional depends on whether `OptionalParams()` call +/// happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// List parameters expect user to enter one or more value of the same type as command's arguments. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +/// +/// [`aliasSourceName`] defines name of the alias source that must be used to auto-resolve this +/// parameter's value. `none` means that parameter will be recorded as-is, any other value +/// (either "weapon", "color", "feature", "entity" or some kind of custom alias source name) will +/// make values prefixed with "$" to be resolved as custom aliases. +public final function ParamTextList( + BaseText name, + optional BaseText variableName, + optional BaseText aliasSourceName +) { local Command.Parameter newParameterValue; if (name == none) { - return self; + return; } newParameterValue = NewParameter(name, CPT_Text, true, variableName); if (aliasSourceName != none) { newParameterValue.aliasSourceName = aliasSourceName.Copy(); } PushParameter(newParameterValue); - return self; -} - -/** - * Adds new remainder parameter (required or optional depends on whether - * `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamRemainder( - BaseText name, - optional BaseText variableName) -{ - if (name == none) { - return self; - } - PushParameter(NewParameter(name, CPT_Remainder, false, variableName)); - return self; -} - -/** - * Adds new object parameter (required or optional depends on whether - * `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamObject( - BaseText name, - optional BaseText variableName) -{ - if (name == none) { - return self; - } - PushParameter(NewParameter(name, CPT_Object, false, variableName)); - return self; -} - -/** - * Adds new parameter for list of objects (required or optional depends on - * whether `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamObjectList( - BaseText name, - optional BaseText variableName) -{ - if (name == none) { - return self; - } - PushParameter(NewParameter(name, CPT_Object, true, variableName)); - return self; -} - -/** - * Adds new array parameter (required or optional depends on whether - * `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamArray( - BaseText name, - optional BaseText variableName) -{ - if (name == none) { - return self; - } - PushParameter(NewParameter(name, CPT_Array, false, variableName)); - return self; -} - -/** - * Adds new parameter for list of arrays (required or optional depends on - * whether `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamArrayList( - BaseText name, - optional BaseText variableName) -{ - if (name == none) { - return self; - } - PushParameter(NewParameter(name, CPT_Array, true, variableName)); - return self; } -/** - * Adds new JSON value parameter (required or optional depends on whether - * `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamJSON( - BaseText name, - optional BaseText variableName) -{ - if (name == none) { - return self; - } - PushParameter(NewParameter(name, CPT_JSON, false, variableName)); - return self; -} - -/** - * Adds new parameter for list of JSON values (required or optional depends on - * whether `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamJSONList( - BaseText name, - optional BaseText variableName) -{ - if (name == none) { - return self; - } - PushParameter(NewParameter(name, CPT_JSON, true, variableName)); - return self; -} - -/** - * Adds new players value parameter (required or optional depends on whether - * `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Players parameter is a parameter that allows one to specify a list of - * players through special selectors, the same way one does for - * targeted commands. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamPlayers( - BaseText name, - optional BaseText variableName) -{ - if (name == none) { - return self; - } - PushParameter(NewParameter(name, CPT_PLAYERS, false, variableName)); - return self; -} - -/** - * Adds new parameter for list of players values (required or optional depends - * on whether `RequireTarget()` call happened) to the currently selected - * sub-command / option. - * - * Players parameter is a parameter that allows one to specify a list of - * players through special selectors, the same way one does for - * targeted commands. - * - * Only fails if provided `name` is `none`. - * - * @param name Name of the parameter, will be copied - * (as it would appear in the generated help info). - * @param variableName Name of the variable that will store this - * parameter's value in `HashTable` after user's command input - * is parsed. Provided value will be copied. - * If left `none`, - will coincide with `name` parameter. - * @return Returns the caller `CommandDataBuilder` to allow for - * method chaining. - */ -public final function CommandDataBuilder ParamPlayersList( - BaseText name, - optional BaseText variableName) -{ - if (name == none) { - return self; +/// Adds new remainder parameter (required or optional depends on whether `OptionalParams()` call +/// happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// Remainder parameter is a special parameter that will simply consume all remaining command's +/// input as-is. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamRemainder(BaseText name, optional BaseText variableName) { + if (name != none) { + PushParameter(NewParameter(name, CPT_Remainder, false, variableName)); + } +} + +/// Adds new JSON object parameter (required or optional depends on whether `OptionalParams()` call +/// happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamObject(BaseText name, optional BaseText variableName) { + if (name != none) { + PushParameter(NewParameter(name, CPT_Object, false, variableName)); + } +} + +/// Adds new JSON object list parameter (required or optional depends on whether `OptionalParams()` +/// call happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// List parameters expect user to enter one or more value of the same type as command's arguments. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamObjectList(BaseText name, optional BaseText variableName) { + if (name != none) { + PushParameter(NewParameter(name, CPT_Object, true, variableName)); + } +} + +/// Adds new JSON array parameter (required or optional depends on whether `OptionalParams()` call +/// happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamArray(BaseText name, optional BaseText variableName) { + if (name != none) { + PushParameter(NewParameter(name, CPT_Array, false, variableName)); + } +} + +/// Adds new JSON array list parameter (required or optional depends on whether `OptionalParams()` +/// call happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// List parameters expect user to enter one or more value of the same type as command's arguments. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamArrayList(BaseText name, optional BaseText variableName) { + if (name != none) { + PushParameter(NewParameter(name, CPT_Array, true, variableName)); + } +} + +/// Adds new JSON value parameter (required or optional depends on whether `OptionalParams()` call +/// happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamJSON(BaseText name, optional BaseText variableName) { + if (name != none) { + PushParameter(NewParameter(name, CPT_JSON, false, variableName)); + } +} + +/// Adds new JSON value list parameter (required or optional depends on whether `OptionalParams()` +/// call happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// List parameters expect user to enter one or more value of the same type as command's arguments. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamJSONList(BaseText name, optional BaseText variableName) { + if (name != none) { + PushParameter(NewParameter(name, CPT_JSON, true, variableName)); + } +} + + +/// Adds new integer parameter (required or optional depends on whether `OptionalParams()` call +/// happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// Players parameter is a parameter that allows one to specify a list of players through +/// special selectors, the same way one does for targeted commands. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamPlayers(BaseText name, optional BaseText variableName) { + if (name != none) { + PushParameter(NewParameter(name, CPT_PLAYERS, false, variableName)); + } +} + +/// Adds new integer list parameter (required or optional depends on whether `OptionalParams()` +/// call happened) to the currently selected sub-command / option. +/// +/// Only fails if provided `name` is `none`. +/// +/// Players parameter is a parameter that allows one to specify a list of players through +/// special selectors, the same way one does for targeted commands. +/// +/// List parameters expect user to enter one or more value of the same type as command's arguments. +/// +/// [`name`] will become the name of the parameter +/// (it would appear in the generated "help" command info). +/// +/// [`variableName`] will become key for this parameter's value in `HashTable` after user's command +/// input is parsed. +/// If left `none`, - will coincide with `name` parameter. +public final function ParamPlayersList(BaseText name,optional BaseText variableName) { + if (name != none) { + PushParameter(NewParameter(name, CPT_PLAYERS, true, variableName)); } - PushParameter(NewParameter(name, CPT_PLAYERS, true, variableName)); - return self; } defaultproperties diff --git a/sources/LevelAPI/Features/Commands/Tests/MockCommandA.uc b/sources/LevelAPI/Features/Commands/Tests/MockCommandA.uc index 48bfbc6..630b1d6 100644 --- a/sources/LevelAPI/Features/Commands/Tests/MockCommandA.uc +++ b/sources/LevelAPI/Features/Commands/Tests/MockCommandA.uc @@ -21,16 +21,17 @@ class MockCommandA extends Command; protected function BuildData(CommandDataBuilder builder) { - builder.ParamObject(P("just_obj")) - .ParamArrayList(P("manyLists")) - .OptionalParams() - .ParamObject(P("last_obj")); - builder.SubCommand(P("simple")) - .ParamBooleanList(P("isItSimple?")) - .ParamInteger(P("integer variable"), P("int")) - .OptionalParams() - .ParamNumberList(P("numeric list"), P("list")) - .ParamTextList(P("another list")); + builder.ParamObject(P("just_obj")); + builder.ParamArrayList(P("manyLists")); + builder.OptionalParams(); + builder.ParamObject(P("last_obj")); + + builder.SubCommand(P("simple")); + builder.ParamBooleanList(P("isItSimple?")); + builder.ParamInteger(P("integer variable"), P("int")); + builder.OptionalParams(); + builder.ParamNumberList(P("numeric list"), P("list")); + builder.ParamTextList(P("another list")); } defaultproperties diff --git a/sources/LevelAPI/Features/Commands/Tests/MockCommandB.uc b/sources/LevelAPI/Features/Commands/Tests/MockCommandB.uc index 49b6896..7f27fdc 100644 --- a/sources/LevelAPI/Features/Commands/Tests/MockCommandB.uc +++ b/sources/LevelAPI/Features/Commands/Tests/MockCommandB.uc @@ -21,31 +21,39 @@ class MockCommandB extends Command; protected function BuildData(CommandDataBuilder builder) { - builder.ParamArray(P("just_array")) - .ParamText(P("just_text")); - builder.Option(P("values")) - .ParamIntegerList(P("types")); - builder.Option(P("long")) - .ParamInteger(P("num")) - .ParamNumberList(P("text")) - .ParamBoolean(P("huh")); - builder.Option(P("type"), P("t")) - .ParamText(P("type")); - builder.Option(P("Test")) - .ParamText(P("to_test")); - builder.Option(P("silent")) - .Option(P("forced")) - .Option(P("verbose"), P("V")) - .Option(P("actual")); - builder.SubCommand(P("do")) - .OptionalParams() - .ParamNumberList(P("numeric list"), P("list")) - .ParamBoolean(P("maybe")); - builder.Option(P("remainder")) - .ParamRemainder(P("everything")); - builder.SubCommand(P("json")) - .ParamJSON(P("first_json")) - .ParamJSONList(P("other_json")); + builder.ParamArray(P("just_array")); + builder.ParamText(P("just_text")); + + builder.Option(P("values")); + builder.ParamIntegerList(P("types")); + + builder.Option(P("long")); + builder.ParamInteger(P("num")); + builder.ParamNumberList(P("text")); + builder.ParamBoolean(P("huh")); + + builder.Option(P("type"), P("t")); + builder.ParamText(P("type")); + + builder.Option(P("Test")); + builder.ParamText(P("to_test")); + + builder.Option(P("silent")); + builder.Option(P("forced")); + builder.Option(P("verbose"), P("V")); + builder.Option(P("actual")); + + builder.SubCommand(P("do")); + builder.OptionalParams(); + builder.ParamNumberList(P("numeric list"), P("list")); + builder.ParamBoolean(P("maybe")); + + builder.Option(P("remainder")); + builder.ParamRemainder(P("everything")); + + builder.SubCommand(P("json")); + builder.ParamJSON(P("first_json")); + builder.ParamJSONList(P("other_json")); } defaultproperties diff --git a/sources/LevelAPI/Features/Commands/Tests/TEST_CommandDataBuilder.uc b/sources/LevelAPI/Features/Commands/Tests/TEST_CommandDataBuilder.uc index a573e52..1494e46 100644 --- a/sources/LevelAPI/Features/Commands/Tests/TEST_CommandDataBuilder.uc +++ b/sources/LevelAPI/Features/Commands/Tests/TEST_CommandDataBuilder.uc @@ -24,27 +24,33 @@ class TEST_CommandDataBuilder extends TestCase protected static function CommandDataBuilder PrepareBuilder() { local CommandDataBuilder builder; - builder = - CommandDataBuilder(__().memory.Allocate(class'CommandDataBuilder')); - builder.ParamNumber(P("var")).ParamText(P("str_var"), P("otherName")); + builder = CommandDataBuilder(__().memory.Allocate(class'CommandDataBuilder')); + builder.ParamNumber(P("var")); + builder.ParamText(P("str_var"), P("otherName")); builder.OptionalParams(); builder.Describe(P("Simple command")); builder.ParamBooleanList(P("list"), PBF_OnOff); // Subcommands - builder.SubCommand(P("sub")).ParamArray(P("array_var")); + builder.SubCommand(P("sub")); + builder.ParamArray(P("array_var")); builder.Describe(P("Alternative command!")); builder.ParamIntegerList(P("int")); builder.SubCommand(P("empty")); builder.Describe(P("Empty one!")); - builder.SubCommand(P("huh")).ParamNumber(P("list")); - builder.SubCommand(P("sub")).ParamObjectList(P("one_more"), P("but")); + builder.SubCommand(P("huh")); + builder.ParamNumber(P("list")); + builder.SubCommand(P("sub")); + builder.ParamObjectList(P("one_more"), P("but")); builder.Describe(P("Alternative command! Updated!")); // Options - builder.Option(P("silent")).Describe(P("Just an option, I dunno.")); + builder.Option(P("silent")); + builder.Describe(P("Just an option, I dunno.")); builder.Option(P("Params"), P("d")); builder.ParamBoolean(P("www"), PBF_YesNo, P("random")); - builder.OptionalParams().ParamIntegerList(P("www2")); - return builder.RequireTarget(); + builder.OptionalParams(); + builder.ParamIntegerList(P("www2")); + builder.RequireTarget(); + return builder; } protected static function Command.SubCommand GetSubCommand( diff --git a/sources/Users/ACommandUserGroups.uc b/sources/Users/ACommandUserGroups.uc index b58f706..62d18ba 100644 --- a/sources/Users/ACommandUserGroups.uc +++ b/sources/Users/ACommandUserGroups.uc @@ -22,56 +22,59 @@ class ACommandUserGroups extends Command protected function BuildData(CommandDataBuilder builder) { - builder.Name(P("usergroups")) - .Group(P("admin")) - .Summary(P("User groups management.")) - .Describe(P("Allows to add/remove user groups and users to these:" - @ "groups. Changes made by it will always affect current session," - @ "but might fail to be saved in case user groups are stored in" - @ "a database that is either corrupted or in read-only mode.")); - builder.SubCommand(P("list")) - .Describe(P("Lists specified groups along with users that belong to" - @ "them. If no groups were specified at all - lists all available" - @ "groups.")) - .OptionalParams() - .ParamTextList(P("groups")); - builder.SubCommand(P("add")) - .Describe(P("Adds a new group")) - .ParamText(P("group_name")); - builder.SubCommand(P("remove")) - .Describe(P("Removes a group")) - .ParamText(P("group_name")); - builder.SubCommand(P("addplayer")) - .Describe(F("Adds new user to the group, specified by the player" - @ "selector. Can add several players at once." - @ "Allows to also optionally specify annotation" - @ "(human-readable name) that can be thought of as" - @ "a {$TextEmphasis comment}. If annotation isn't specified" - @ "current nickname will be used as one.")) - .ParamText(P("group_name")) - .ParamPlayers(P("player_selector")) - .OptionalParams() - .ParamText(P("annotation")); - builder.SubCommand(P("removeplayer")) - .Describe(P("Removes user from the group, specified by player selector." - @ "Can remove several players at once.")) - .ParamText(P("group_name")) - .ParamPlayers(P("player_selector")); - builder.SubCommand(P("adduser")) - .Describe(F("Adds new user to the group. Allows to also optionally" - @ "specify annotation (human-readable name) that can be thought of" - @ "as a {$TextEmphasis comment}.")) - .ParamText(P("group_name")) - .ParamText(P("user_id")) - .OptionalParams() - .ParamText(P("annotation")); - builder.SubCommand(P("removeuser")) - .Describe(P("Removes user from the group. User can be specified by both" - @ "user's id or annotation, with id taking priority.")) - .ParamText(P("group_name")) - .ParamText(P("user_name")); - builder.Option(P("force")) - .Describe(P("Allows to force usage of invalid user IDs.")); + builder.Name(P("usergroups")); + builder.Group(P("admin")); + builder.Summary(P("User groups management.")); + builder.Describe(P("Allows to add/remove user groups and users to these: groups. Changes made" + @ "by it will always affect current session, but might fail to be saved in case user groups" + @ "are stored in a database that is either corrupted or in read-only mode.")); + + builder.SubCommand(P("list")); + builder.Describe(P("Lists specified groups along with users that belong to them. If no groups" + @ "were specified at all - lists all available groups.")); + builder.OptionalParams(); + builder.ParamTextList(P("groups")); + + builder.SubCommand(P("add")); + builder.Describe(P("Adds a new group")); + builder.ParamText(P("group_name")); + + builder.SubCommand(P("remove")); + builder.Describe(P("Removes a group")); + builder.ParamText(P("group_name")); + + builder.SubCommand(P("addplayer")); + builder.Describe(F("Adds new user to the group, specified by the player selector. Can add" + @ "several players at once. Allows to also optionally specify annotation (human-readable" + @ "name) that can be thought of as a {$TextEmphasis comment}. If annotation isn't" + @ "specified current nickname will be used as one.")); + builder.ParamText(P("group_name")); + builder.ParamPlayers(P("player_selector")); + builder.OptionalParams(); + builder.ParamText(P("annotation")); + + builder.SubCommand(P("removeplayer")); + builder.Describe(P("Removes user from the group, specified by player selector." + @ "Can remove several players at once.")); + builder.ParamText(P("group_name")); + builder.ParamPlayers(P("player_selector")); + + builder.SubCommand(P("adduser")); + builder.Describe(F("Adds new user to the group. Allows to also optionally specify annotation" + @ "(human-readable name) that can be thought of as a {$TextEmphasis comment}.")); + builder.ParamText(P("group_name")); + builder.ParamText(P("user_id")); + builder.OptionalParams(); + builder.ParamText(P("annotation")); + + builder.SubCommand(P("removeuser")); + builder.Describe(P("Removes user from the group. User can be specified by both user's id or" + @ "annotation, with id taking priority.")); + builder.ParamText(P("group_name")); + builder.ParamText(P("user_name")); + + builder.Option(P("force")); + builder.Describe(P("Allows to force usage of invalid user IDs.")); } protected function Executed(CallData arguments, EPlayer instigator) From 06915cbddf2737820bf98ab4694b3f63ea63034f Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 13 Mar 2023 04:27:21 +0700 Subject: [PATCH 02/17] Change auto-alias resolving in commands to be useful Previously auto-resolved aliases overwrote user-provided value, which made it difficult to print response/error messages as a result of a command, rendering auto-resolving unusable. This patch fixes that problem by recording both alias and resolved value inside a `HashTable` value. --- .../Features/Commands/CommandDataBuilder.uc | 8 +++ .../Features/Commands/CommandParser.uc | 49 ++++++++++++------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/sources/LevelAPI/Features/Commands/CommandDataBuilder.uc b/sources/LevelAPI/Features/Commands/CommandDataBuilder.uc index 8232917..03fd804 100644 --- a/sources/LevelAPI/Features/Commands/CommandDataBuilder.uc +++ b/sources/LevelAPI/Features/Commands/CommandDataBuilder.uc @@ -637,6 +637,10 @@ public final function ParamNumberList(BaseText name, optional BaseText variableN /// parameter's value. `none` means that parameter will be recorded as-is, any other value /// (either "weapon", "color", "feature", "entity" or some kind of custom alias source name) will /// make values prefixed with "$" to be resolved as custom aliases. +/// In case auto-resolving is used, value will be recorded as a `HasTable` with two fields: +/// "alias" - value provided by user and (in case "$" prefix was used) "value" - actual resolved +/// value of an alias. +/// If alias has failed to be resolved, `none` will be stored as a value. public final function ParamText( BaseText name, optional BaseText variableName, @@ -672,6 +676,10 @@ public final function ParamText( /// parameter's value. `none` means that parameter will be recorded as-is, any other value /// (either "weapon", "color", "feature", "entity" or some kind of custom alias source name) will /// make values prefixed with "$" to be resolved as custom aliases. +/// In case auto-resolving is used, value will be recorded as a `HasTable` with two fields: +/// "alias" - value provided by user and (in case "$" prefix was used) "value" - actual resolved +/// value of an alias. +/// If alias has failed to be resolved, `none` will be stored as a value. public final function ParamTextList( BaseText name, optional BaseText variableName, diff --git a/sources/LevelAPI/Features/Commands/CommandParser.uc b/sources/LevelAPI/Features/Commands/CommandParser.uc index 614fa06..afb1c70 100644 --- a/sources/LevelAPI/Features/Commands/CommandParser.uc +++ b/sources/LevelAPI/Features/Commands/CommandParser.uc @@ -616,9 +616,10 @@ private final function bool ParseTextValue( HashTable parsedParameters, Command.Parameter expectedParameter) { - local bool failedParsing; - local MutableText textValue; - local Parser.ParserState initialState; + local bool failedParsing; + local MutableText textValue; + local Parser.ParserState initialState; + local HashTable resolvedPair; // (needs some work for reading formatting `string`s from `Text` objects) initialState = commandParser.Skip().GetCurrentState(); @@ -639,24 +640,35 @@ private final function bool ParseTextValue( commandParser.Fail(); return false; } - AutoResolveAlias(textValue, expectedParameter.aliasSourceName); - RecordParameter(parsedParameters, expectedParameter, textValue.IntoText()); + resolvedPair = AutoResolveAlias(textValue, expectedParameter.aliasSourceName); + if (resolvedPair != none) { + RecordParameter(parsedParameters, expectedParameter, resolvedPair); + _.memory.Free(textValue); + } else { + RecordParameter(parsedParameters, expectedParameter, textValue.IntoText()); + } return true; } -// Resolves alias with appropriate source, if parameter was specified to be -// auto-resolved. -// Resolved values is returned through first out-parameter. -private final function AutoResolveAlias( - out MutableText textValue, - Text aliasSourceName) -{ - local Text resolvedValue; +// Resolves alias and returns it, along with the resolved value, if parameter was specified to be +// auto-resolved. +// Returns `none` otherwise. +private final function HashTable AutoResolveAlias(MutableText textValue, Text aliasSourceName) { + local HashTable result; + local Text resolvedValue, immutableValue; - if (textValue == none) return; - if (aliasSourceName == none) return; - if (!textValue.StartsWithS("$")) return; + if (textValue == none) return none; + if (aliasSourceName == none) return none; + // Always create `HashTable` with at least "alias" key + result = _.collections.EmptyHashTable(); + immutableValue = textValue.Copy(); + result.SetItem(P("alias"), immutableValue); + _.memory.Free(immutableValue); + // Add "value" key only after we've checked for "$" prefix + if (!textValue.StartsWithS("$")) { + return result; + } if (aliasSourceName.Compare(P("weapon"))) { resolvedValue = _.alias.ResolveWeapon(textValue, true); } @@ -672,8 +684,9 @@ private final function AutoResolveAlias( else { resolvedValue = _.alias.ResolveCustom(aliasSourceName, textValue, true); } - textValue.FreeSelf(); - textValue = resolvedValue.IntoMutableText(); + result.SetItem(P("value"), resolvedValue); + _.memory.Free2(resolvedValue, immutableValue); + return result; } // Assumes `commandParser` and `parsedParameters` are not `none`. From 58d1d686b9950f21766f68208ee5b1b5a0644b22 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 13 Mar 2023 15:33:46 +0700 Subject: [PATCH 03/17] Add auto-highlighting of commands' descriptions --- .../Features/Commands/CommandDataBuilder.uc | 39 +++++++++++++++++-- sources/Users/ACommandUserGroups.uc | 10 ++--- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/sources/LevelAPI/Features/Commands/CommandDataBuilder.uc b/sources/LevelAPI/Features/Commands/CommandDataBuilder.uc index 03fd804..b06c8c0 100644 --- a/sources/LevelAPI/Features/Commands/CommandDataBuilder.uc +++ b/sources/LevelAPI/Features/Commands/CommandDataBuilder.uc @@ -372,12 +372,45 @@ public final function Option(BaseText longName, optional BaseText shortName) { /// Adds description to the selected sub-command / option. /// +/// Highlights parts of the description in-between "`" characters. +/// /// Does nothing if nothing is yet selected. public final function Describe(BaseText description) { - _.memory.Free(selectedDescription); - if (description != none) { - selectedDescription = description.Copy(); + local int fromIndex, toIndex; + local BaseText.Formatting keyWordFormatting; + local bool lookingForEnd; + local MutableText coloredDescription; + + if (description == none) { + return; + } + keyWordFormatting = _.text.FormattingFromColor(_.color.TextEmphasis); + coloredDescription = description.MutableCopy(); + while (true) { + if (lookingForEnd) { + toIndex = coloredDescription.IndexOf(P("`"), fromIndex + 1); + } else { + fromIndex = coloredDescription.IndexOf(P("`"), toIndex + 1); + } + if (toIndex < 0 || fromIndex < 0) { + break; + } + if (lookingForEnd) { + coloredDescription.ChangeFormatting( + keyWordFormatting, + fromIndex, + toIndex - fromIndex + 1); + lookingForEnd = false; + } else { + lookingForEnd = true; + } } + coloredDescription.Replace(P("`"), P("")); + if (lookingForEnd) { + coloredDescription.ChangeFormatting(keyWordFormatting, fromIndex); + } + _.memory.Free(selectedDescription); + selectedDescription = coloredDescription.IntoText(); } /// Sets new name of the command that caller [`CommandDataBuilder`] constructs. diff --git a/sources/Users/ACommandUserGroups.uc b/sources/Users/ACommandUserGroups.uc index 62d18ba..77c0e22 100644 --- a/sources/Users/ACommandUserGroups.uc +++ b/sources/Users/ACommandUserGroups.uc @@ -44,10 +44,10 @@ protected function BuildData(CommandDataBuilder builder) builder.ParamText(P("group_name")); builder.SubCommand(P("addplayer")); - builder.Describe(F("Adds new user to the group, specified by the player selector. Can add" + builder.Describe(P("Adds new user to the group, specified by the player selector. Can add" @ "several players at once. Allows to also optionally specify annotation (human-readable" - @ "name) that can be thought of as a {$TextEmphasis comment}. If annotation isn't" - @ "specified current nickname will be used as one.")); + @ "name) that can be thought of as a `comment`. If annotation isn't specified current" + @ "nickname will be used as one.")); builder.ParamText(P("group_name")); builder.ParamPlayers(P("player_selector")); builder.OptionalParams(); @@ -60,8 +60,8 @@ protected function BuildData(CommandDataBuilder builder) builder.ParamPlayers(P("player_selector")); builder.SubCommand(P("adduser")); - builder.Describe(F("Adds new user to the group. Allows to also optionally specify annotation" - @ "(human-readable name) that can be thought of as a {$TextEmphasis comment}.")); + builder.Describe(P("Adds new user to the group. Allows to also optionally specify annotation" + @ "(human-readable name) that can be thought of as a `comment`.")); builder.ParamText(P("group_name")); builder.ParamText(P("user_id")); builder.OptionalParams(); From 087d8624d35e57b0bb7f09ce723ff6da0e3984d8 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 13 Mar 2023 16:11:52 +0700 Subject: [PATCH 04/17] Change how config is swapped for `Feature`s Before config was allowed to be swapped while `Feature` was enabled, which has led to a lot of loading code being moved into the `SwapConfig()` method, making `Feature` initialization code way more complex than it needed to be. This patch forces `Feature` to get disabled before config swap. This adds some verhead for swapping configs for already running `Feature`s, but that's a small cost for way simpler implementations. --- sources/Aliases/Aliases_Feature.uc | 3 -- sources/Features/Feature.uc | 28 +++++--------- .../Features/Commands/Commands_Feature.uc | 38 ++++++++----------- sources/Users/Users_Feature.uc | 21 ++++------ 4 files changed, 33 insertions(+), 57 deletions(-) diff --git a/sources/Aliases/Aliases_Feature.uc b/sources/Aliases/Aliases_Feature.uc index 002f628..887d0ed 100644 --- a/sources/Aliases/Aliases_Feature.uc +++ b/sources/Aliases/Aliases_Feature.uc @@ -112,15 +112,12 @@ protected function SwapConfig(FeatureConfig config) if (newConfig == none) { return; } - _.memory.Free(weaponAliasSource); - DropSources(); weaponAliasSource = GetSource(newConfig.weaponAliasSource); colorAliasSource = GetSource(newConfig.colorAliasSource); featureAliasSource = GetSource(newConfig.featureAliasSource); entityAliasSource = GetSource(newConfig.entityAliasSource); commandAliasSource = GetSource(newConfig.commandAliasSource); LoadCustomSources(newConfig.customSource); - _.alias._reloadSources(); } private function LoadCustomSources( diff --git a/sources/Features/Feature.uc b/sources/Features/Feature.uc index 287522f..0de09a2 100644 --- a/sources/Features/Feature.uc +++ b/sources/Features/Feature.uc @@ -170,7 +170,6 @@ public static final function LoadConfigs() /** * Changes config for the caller `Feature` class. * - * This method should only be called when caller `Feature` is enabled. * To set initial config on this `Feature`'s start - specify it as a parameter * to `EnableMe()` method. * @@ -179,20 +178,17 @@ public static final function LoadConfigs() */ private final function ApplyConfig(BaseText newConfigName) { - local Text configNameCopy; + local Text configNameCopy; local FeatureConfig newConfig; - newConfig = - FeatureConfig(configClass.static.GetConfigInstance(newConfigName)); - if (newConfig == none) - { + + newConfig = FeatureConfig(configClass.static.GetConfigInstance(newConfigName)); + if (newConfig == none) { _.logger.Auto(errorBadConfigData).ArgClass(class); // Fallback to "default" config configNameCopy = _.text.FromString(defaultConfigName); configClass.static.NewConfig(configNameCopy); - newConfig = - FeatureConfig(configClass.static.GetConfigInstance(configNameCopy)); - } - else { + newConfig = FeatureConfig(configClass.static.GetConfigInstance(configNameCopy)); + } else { configNameCopy = newConfigName.Copy(); } SwapConfig(newConfig); @@ -289,12 +285,8 @@ public static final function bool IsEnabled() public static final function Feature EnableMe(BaseText configName) { local Feature myInstance; - myInstance = GetEnabledInstance(); - if (myInstance != none) - { - myInstance.ApplyConfig(configName); - return myInstance; - } + + DisableMe(); myInstance = Feature(__().memory.Allocate(default.class)); __().environment.EnableFeature(myInstance, configName); return myInstance; @@ -336,8 +328,8 @@ protected function OnEnabled(){} protected function OnDisabled(){} /** - * Will be called whenever caller `Feature` class must change it's config - * parameters. This can be done both when the `Feature` is enabled or disabled. + * Will be called whenever caller `Feature` class must change it's config parameters. + * It is guaranteed that `Feature` will be disabled during this call. * * @param newConfigData New config that caller `Feature`'s class must use. * We pass `FeatureConfig` value for performance and simplicity reasons, diff --git a/sources/LevelAPI/Features/Commands/Commands_Feature.uc b/sources/LevelAPI/Features/Commands/Commands_Feature.uc index 5e9166e..3b41566 100644 --- a/sources/LevelAPI/Features/Commands/Commands_Feature.uc +++ b/sources/LevelAPI/Features/Commands/Commands_Feature.uc @@ -3,7 +3,7 @@ * parse their arguments into standard Acedia collection. It also allows to * manage them (and specify limitation on how they can be called) in a * centralized manner. - * Copyright 2021-2022 Anton Tarasenko + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -117,6 +117,18 @@ protected function OnEnabled() commandDelimiters[2] = _.text.FromString("["); // Negation of the selector commandDelimiters[3] = _.text.FromString("!"); + if (useChatInput) { + _.chat.OnMessage(self).connect = HandleCommands; + } + else { + _.chat.OnMessage(self).Disconnect(); + } + if (useMutateInput || emergencyEnabledMutate) { + _server.unreal.mutator.OnMutate(self).connect = HandleMutate; + } + else { + _server.unreal.mutator.OnMutate(self).Disconnect(); + } } protected function OnDisabled() @@ -150,28 +162,8 @@ protected function SwapConfig(FeatureConfig config) _.memory.Free(chatCommandPrefix); chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix); allowedPlayers = newConfig.allowedPlayers; - if (useChatInput != newConfig.useChatInput) - { - useChatInput = newConfig.useChatInput; - if (newConfig.useChatInput) { - _.chat.OnMessage(self).connect = HandleCommands; - } - else { - _.chat.OnMessage(self).Disconnect(); - } - } - // Do not make any modifications here in case "mutate" was - // emergency-enabled - if (useMutateInput != newConfig.useMutateInput && !emergencyEnabledMutate) - { - useMutateInput = newConfig.useMutateInput; - if (newConfig.useMutateInput) { - _server.unreal.mutator.OnMutate(self).connect = HandleMutate; - } - else { - _server.unreal.mutator.OnMutate(self).Disconnect(); - } - } + useChatInput = newConfig.useChatInput; + useMutateInput = newConfig.useMutateInput; } /** diff --git a/sources/Users/Users_Feature.uc b/sources/Users/Users_Feature.uc index e785eb5..1d15c7a 100644 --- a/sources/Users/Users_Feature.uc +++ b/sources/Users/Users_Feature.uc @@ -80,7 +80,13 @@ protected function OnEnabled() feature.RegisterCommand(class'ACommandUserGroups'); feature.FreeSelf(); } - LoadUserData(); + if (_server.IsAvailable()) { + LoadUserData(); + SetupPersistentData(usePersistentData); + } else { + _.logger.Auto(errNoServerCore); + return; + } } protected function OnDisabled() @@ -113,17 +119,6 @@ protected function SwapConfig(FeatureConfig config) useDatabaseForGroupsData = newConfig.useDatabaseForGroupsData; groupsDatabaseLink = newConfig.groupsDatabaseLink; availableUserGroups = newConfig.localUserGroup; - ResetUploadedUserGroups(); - if (IsEnabled()) - { - if (!_server.IsAvailable()) - { - _.logger.Auto(errNoServerCore); - return; - } - LoadUserData(); - SetupPersistentData(usePersistentData); - } } /** @@ -2153,6 +2148,6 @@ defaultproperties errDBBadRootUserGroupData = (l=LOG_Error,m="Database link \"%1\" (configured to load user group data in \"AcediaUsers.ini\") contains incompatible data.") errDBBadLinkPointer = (l=LOG_Error,m="Path inside database link \"%1\" (configured inside \"AcediaUsers.ini\") is invalid.") errDBDamaged = (l=LOG_Error,m="Database given by the link \"%1\" (configured inside \"AcediaUsers.ini\") seems to be damaged.") - errNoServerCore = (l=LOG_Error,m="Cannot start \"Users_Feature\", because no `ServerCore` was created.") + errNoServerCore = (l=LOG_Error,m="\"Users_Feature\" won't function properly, because no `ServerCore` was created.") errDBContainsNonLowerRegister = (l=LOG_Error,m="Database given by the link \"%1\" contains non-lower case key \"%2\". This shouldn't happen, unless someone manually edited database.") } \ No newline at end of file From 41909851f572a0385eb5d3c2264685f31fd2b120 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 13 Mar 2023 18:29:42 +0700 Subject: [PATCH 05/17] Refactor `Commands_Feature` to use API --- sources/BaseAPI/Global.uc | 3 + .../LevelAPI/Features/Commands/CommandAPI.uc | 114 +++ .../LevelAPI/Features/Commands/Commands.uc | 64 +- .../Features/Commands/Commands_Feature.uc | 676 ++++++++---------- 4 files changed, 419 insertions(+), 438 deletions(-) create mode 100644 sources/LevelAPI/Features/Commands/CommandAPI.uc diff --git a/sources/BaseAPI/Global.uc b/sources/BaseAPI/Global.uc index e87cb2f..f53553f 100644 --- a/sources/BaseAPI/Global.uc +++ b/sources/BaseAPI/Global.uc @@ -50,6 +50,7 @@ var public UserAPI users; var public PlayersAPI players; var public JsonAPI json; var public SchedulerAPI scheduler; +var public CommandAPI commands; var public AvariceAPI avarice; var public AcediaEnvironment environment; @@ -92,6 +93,7 @@ protected function Initialize() { players = PlayersAPI(memory.Allocate(class'PlayersAPI')); scheduler = SchedulerAPI(memory.Allocate(class'SchedulerAPI')); avarice = AvariceAPI(memory.Allocate(class'AvariceAPI')); + commands = CommandAPI(memory.Allocate(class'CommandAPI')); environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment')); } @@ -112,6 +114,7 @@ public function DropCoreAPI() { players = none; json = none; scheduler = none; + commands = none; avarice = none; default.myself = none; } diff --git a/sources/LevelAPI/Features/Commands/CommandAPI.uc b/sources/LevelAPI/Features/Commands/CommandAPI.uc new file mode 100644 index 0000000..8b46965 --- /dev/null +++ b/sources/LevelAPI/Features/Commands/CommandAPI.uc @@ -0,0 +1,114 @@ +/** + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 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 CommandAPI extends AcediaObject; + +var private Commands_Feature commandsFeature; + +// DO NOT CALL MANUALLY +public final /*internal*/ function _reloadFeature() +{ + if (commandsFeature != none) { + commandsFeature.FreeSelf(); + commandsFeature = none; + } + commandsFeature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); +} + +/// Checks if `Commands_Feature` is enabled, which is required for this API to be functional. +public final function bool AreCommandsEnabled() { + // `Commands_Feature` is responsible for updating us with an actually enabled instance + return (commandsFeature != none); +} + +/// Registers given command class, making it available via `Execute()`. +/// +/// Returns `true` if command was successfully registered and `false` otherwise`. +/// +/// # Errors +/// +/// If `commandClass` provides command with a name that is already taken (comparison is +/// case-insensitive) by a different command - a warning will be logged and newly passed +/// `commandClass` discarded. +public final function bool RegisterCommand(class commandClass) { + if (commandsFeature != none) { + return commandsFeature.RegisterCommand(commandClass); + } + return false; +} + +/// Removes command of given class from the list of registered commands. +/// +/// Removing once registered commands is not an action that is expected to be performed under normal +/// circumstances and it is not efficient. +/// It is linear on the current amount of commands. +public final function RemoveCommand(class commandClass) { + if (commandsFeature != none) { + commandsFeature.RemoveCommand(commandClass); + } +} + +/// Executes command based on the input. +/// +/// Takes [`commandLine`] as input with command's call, finds appropriate registered command +/// instance and executes it with parameters specified in the [`commandLine`]. +/// +/// [`callerPlayer`] has to be specified and represents instigator of this command that will receive +/// appropriate result/error messages. +/// +/// Returns `true` iff command was successfully executed. +/// +/// # Errors +/// +/// Doesn't log any errors, but can complain about errors in name or parameters to +/// the [`callerPlayer`] +public final function Execute(BaseText commandLine, EPlayer callerPlayer) { + if (commandsFeature != none) { + commandsFeature.HandleInput(commandLine, callerPlayer); + } +} + +/// Executes command based on the input. +/// +/// Takes [`commandLine`] as input with command's call, finds appropriate registered command +/// instance and executes it with parameters specified in the [`commandLine`]. +/// +/// [`callerPlayer`] has to be specified and represents instigator of this command that will receive +/// appropriate result/error messages. +/// +/// Returns `true` iff command was successfully executed. +/// +/// # Errors +/// +/// Doesn't log any errors, but can complain about errors in name or parameters to +/// the [`callerPlayer`] +public final function Execute_S(string commandLine, EPlayer callerPlayer) { + local MutableText wrapper; + + if (commandsFeature != none) { + wrapper = _.text.FromStringM(commandLine); + commandsFeature.HandleInput(wrapper, callerPlayer); + _.memory.Free(wrapper); + } +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/LevelAPI/Features/Commands/Commands.uc b/sources/LevelAPI/Features/Commands/Commands.uc index 35138ca..82f5ba7 100644 --- a/sources/LevelAPI/Features/Commands/Commands.uc +++ b/sources/LevelAPI/Features/Commands/Commands.uc @@ -1,6 +1,8 @@ /** - * Config object for `Commands_Feature`. - * Copyright 2021-2022 Anton Tarasenko + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -21,64 +23,38 @@ class Commands extends FeatureConfig perobjectconfig config(AcediaSystem); -var public config bool useChatInput; -var public config bool useMutateInput; -var public config string chatCommandPrefix; -var public config array allowedPlayers; +var public config bool useChatInput; +var public config bool useMutateInput; +var public config string chatCommandPrefix; -protected function HashTable ToData() -{ - local int i; +protected function HashTable ToData() { local HashTable data; - local ArrayList playerList; data = __().collections.EmptyHashTable(); data.SetBool(P("useChatInput"), useChatInput, true); data.SetBool(P("useMutateInput"), useMutateInput, true); data.SetString(P("chatCommandPrefix"), chatCommandPrefix); - playerList = _.collections.EmptyArrayList(); - for (i = 0; i < allowedPlayers.length; i += 1) { - playerList.AddString(allowedPlayers[i]); - } - data.SetItem(P("allowedPlayers"), playerList); - playerList.FreeSelf(); return data; } -protected function FromData(HashTable source) -{ - local int i; - local ArrayList playerList; - +protected function FromData(HashTable source) { if (source == none) { return; } - useChatInput = source.GetBool(P("useChatInput")); - useMutateInput = source.GetBool(P("useMutateInput")); - chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!"); - playerList = source.GetArrayList(P("allowedPlayers")); - allowedPlayers.length = 0; - if (playerList == none) { - return; - } - for (i = 0; i < playerList.GetLength(); i += 1) { - allowedPlayers[allowedPlayers.length] = playerList.GetString(i); - } - playerList.FreeSelf(); + useChatInput = source.GetBool(P("useChatInput")); + useMutateInput = source.GetBool(P("useMutateInput")); + chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!"); } -protected function DefaultIt() -{ - useChatInput = true; - useMutateInput = true; - chatCommandPrefix = "!"; - allowedPlayers.length = 0; +protected function DefaultIt() { + useChatInput = true; + useMutateInput = true; + chatCommandPrefix = "!"; } -defaultproperties -{ +defaultproperties { configName = "AcediaSystem" - useChatInput = true - useMutateInput = true - chatCommandPrefix = "!" + useChatInput = true + useMutateInput = true + chatCommandPrefix = "!" } \ No newline at end of file diff --git a/sources/LevelAPI/Features/Commands/Commands_Feature.uc b/sources/LevelAPI/Features/Commands/Commands_Feature.uc index 3b41566..30c0ec3 100644 --- a/sources/LevelAPI/Features/Commands/Commands_Feature.uc +++ b/sources/LevelAPI/Features/Commands/Commands_Feature.uc @@ -1,9 +1,8 @@ /** - * This feature provides a mechanism to define commands that automatically - * parse their arguments into standard Acedia collection. It also allows to - * manage them (and specify limitation on how they can be called) in a - * centralized manner. - * Copyright 2021-2023 Anton Tarasenko + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -22,100 +21,72 @@ */ class Commands_Feature extends Feature; -/** - * # `Commands_Feature` - * - * This feature provides a mechanism to define commands that automatically - * parse their arguments into standard Acedia collection. It also allows to - * manage them (and specify limitation on how they can be called) in a - * centralized manner. - * Support command input from chat and "mutate" command. - * - * ## Usage - * - * Should be enabled like any other feature. Additionally support - * `EmergencyEnable()` enabling method that bypasses regular settings to allow - * admins to start this feature while forcefully enabling "mutate" command - * input method. - * Available configuration: - * - * 1. Whether to use command input from chat and what prefix is used to - * denote a command (by default "!"); - * 2. Whether to use command input from "mutate" command. - * - * To add new commands into the system - get enabled instance of this - * feature and call its `RegisterCommand()` method to add your custom - * `Command` class. `RemoveCommand()` can also be used to de-register - * a command, if you need this for some reason. - * - * ## Implementation - * - * Implementation is simple: calling a method `RegisterCommand()` adds - * command into two caches `registeredCommands` for obtaining registered - * commands by name and `groupedCommands` for obtaining arrays of commands by - * their group name. These arrays are used for providing methods for fetching - * arrays of commands and obtaining pre-allocated `Command` instances by their - * name. - * Depending on settings, this feature also connects to corresponding - * signals for catching "mutate"/chat input, then it checks user-specified name - * for being an alias and picks correct command from `registeredCommands`. - * Emergency enabling this feature sets `emergencyEnabledMutate` flag that - * enforces connecting to the "mutate" input. - */ +//! This feature manages commands that automatically parse their arguments into standard Acedia +//! collections. +//! +//! # Implementation +//! +//! Implementation is simple: calling a method `RegisterCommand()` adds +//! command into two caches `registeredCommands` for obtaining registered +//! commands by name and `groupedCommands` for obtaining arrays of commands by +//! their group name. These arrays are used for providing methods for fetching +//! arrays of commands and obtaining pre-allocated `Command` instances by their +//! name. +//! Depending on settings, this feature also connects to corresponding +//! signals for catching "mutate"/chat input, then it checks user-specified name +//! for being an alias and picks correct command from `registeredCommands`. +//! Emergency enabling this feature sets `emergencyEnabledMutate` flag that +//! enforces connecting to the "mutate" input. + +// Auxiliary struct for passing name of the command to call plus, optionally, additional +// sub-command name. +// +// Normally sub-command name is parsed by the command itself, however command aliases can try to +// enforce one. +struct CommandCallPair { + var MutableText commandName; + // In case it is enforced by an alias + var MutableText subCommandName; +}; -// Delimiters that always separate command name from it's parameters +// Delimiters that always separate command name from it's parameters var private array commandDelimiters; -// Registered commands, recorded as (, ) pairs. -// Keys should be deallocated when their entry is removed. +// Registered commands, recorded as (, ) pairs. +// Keys should be deallocated when their entry is removed. var private HashTable registeredCommands; -// `HashTable` of "" <-> `ArrayList` of commands pairs -// to allow quick fetch of commands belonging to a single group +// `HashTable` of "" <-> `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 +// When this flag is set to true, mutate input becomes available despite `useMutateInput` flag to +// allow to unlock server in case of an error var private bool emergencyEnabledMutate; -// Setting this to `true` enables players to input commands right in the chat -// by prepending them with `chatCommandPrefix`. -// Default is `true`. +// Setting this to `true` enables players to input commands right in the chat by prepending them +// with `chatCommandPrefix`. +// Default is `true`. var private /*config*/ bool useChatInput; -// Setting this to `true` enables players to input commands with "mutate" -// console command. -// Default is `true`. +// Setting this to `true` enables players to input commands with "mutate" console command. +// Default is `true`. var private /*config*/ bool useMutateInput; -// Chat messages, prepended by this prefix will be treated as commands. -// Default is "!". Empty values are also treated as "!". +// Chat messages, prepended by this prefix will be treated as commands. +// Default is "!". Empty values are also treated as "!". var private /*config*/ Text chatCommandPrefix; -// List of steam IDs of players allowed to use commands. -// Temporary measure until a better solution is finished. -var private /*config*/ array allowedPlayers; - -// Contains name of the command to call plus, optionally, -// additional sub-command name. -// Normally sub-command name is parsed by the command itself, however -// command aliases can try to enforce one. -struct CommandCallPair -{ - var MutableText commandName; - // In case it is enforced by an alias - var MutableText subCommandName; -}; var LoggerAPI.Definition errCommandDuplicate; -protected function OnEnabled() -{ - registeredCommands = _.collections.EmptyHashTable(); - groupedCommands = _.collections.EmptyHashTable(); +protected function OnEnabled() { + registeredCommands = _.collections.EmptyHashTable(); + groupedCommands = _.collections.EmptyHashTable(); RegisterCommand(class'ACommandHelp'); - // Macro selector + // Macro selector commandDelimiters[0] = _.text.FromString("@"); - // Key selector + // Key selector commandDelimiters[1] = _.text.FromString("#"); - // Player array (possibly JSON array) + // Player array (possibly JSON array) commandDelimiters[2] = _.text.FromString("["); - // Negation of the selector + // Negation of the selector + // NOT the same thing as default command prefix in chat commandDelimiters[3] = _.text.FromString("!"); if (useChatInput) { _.chat.OnMessage(self).connect = HandleCommands; @@ -131,23 +102,19 @@ protected function OnEnabled() } } -protected function OnDisabled() -{ +protected function OnDisabled() { if (useChatInput) { _.chat.OnMessage(self).Disconnect(); } if (useMutateInput) { _server.unreal.mutator.OnMutate(self).Disconnect(); } - useChatInput = false; - useMutateInput = false; - _.memory.Free(registeredCommands); - _.memory.Free(groupedCommands); - _.memory.Free(chatCommandPrefix); - _.memory.FreeMany(commandDelimiters); - registeredCommands = none; - groupedCommands = none; - chatCommandPrefix = none; + useChatInput = false; + useMutateInput = false; + _.memory.Free3(registeredCommands, groupedCommands, chatCommandPrefix); + registeredCommands = none; + groupedCommands = none; + chatCommandPrefix = none; commandDelimiters.length = 0; } @@ -161,50 +128,131 @@ protected function SwapConfig(FeatureConfig config) } _.memory.Free(chatCommandPrefix); chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix); - allowedPlayers = newConfig.allowedPlayers; useChatInput = newConfig.useChatInput; useMutateInput = newConfig.useMutateInput; } -/** - * `Command_Feature` is a critical command to have running on your server and, - * if disabled by accident, there will be no way of starting it again without - * restarting the level or even editing configs. - * - * This method allows to enable it along with "mutate" input in case something - * goes wrong. - */ -public final static function EmergencyEnable() -{ - local Text autoConfig; - local Commands_Feature feature; +// Parses command's name into `CommandCallPair` - sub-command is filled in case +// specified name is an alias with specified sub-command name. +private final function CommandCallPair ParseCommandCallPairWith(Parser parser) { + local Text resolvedValue; + local MutableText userSpecifiedName; + local CommandCallPair result; + local Text.Character dotCharacter; + + if (parser == none) return result; + if (!parser.Ok()) return result; + + parser.MUntilMany(userSpecifiedName, commandDelimiters, true, true); + resolvedValue = _.alias.ResolveCommand(userSpecifiedName); + // This isn't an alias + if (resolvedValue == none) { + result.commandName = userSpecifiedName; + return result; + } + // It is an alias - parse it + dotCharacter = _.text.GetCharacter("."); + resolvedValue.Parse() + .MUntil(result.commandName, dotCharacter) + .MatchS(".") + .MUntil(result.subCommandName, dotCharacter) + .FreeSelf(); + if (result.subCommandName.IsEmpty()) { + result.subCommandName.FreeSelf(); + result.subCommandName = none; + } + resolvedValue.FreeSelf(); + return result; +} - if (!IsEnabled()) - { +private function bool HandleCommands(EPlayer sender, MutableText message, bool teamMessage) { + local Parser parser; + + // We are only interested in messages that start with `chatCommandPrefix` + parser = _.text.Parse(message); + if (!parser.Match(chatCommandPrefix).Ok()) { + parser.FreeSelf(); + return true; + } + // Pass input to command feature + HandleInputWith(parser, sender); + parser.FreeSelf(); + return false; +} + +private function HandleMutate(string command, PlayerController sendingPlayer) { + local Parser parser; + local EPlayer sender; + + // A lot of other mutators use these commands + if (command ~= "help") return; + if (command ~= "version") return; + if (command ~= "status") return; + if (command ~= "credits") return; + + parser = _.text.ParseString(command); + sender = _.players.FromController(sendingPlayer); + HandleInputWith(parser, sender); + sender.FreeSelf(); + parser.FreeSelf(); +} + +private final function RemoveClassFromGroup(class 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); +} + +/// This method allows to forcefully enable `Command_Feature` along with "mutate" input in case +/// something goes wrong. +/// +/// `Command_Feature` is a critical command to have running on your server and, +/// if disabled by accident, there will be no way of starting it again without +/// restarting the level or even editing configs. +public final static function EmergencyEnable() { + local bool noWayToInputCommands; + local Text autoConfig; + local Commands_Feature feature; + + if (!IsEnabled()) { autoConfig = GetAutoEnabledConfig(); EnableMe(autoConfig); __().memory.Free(autoConfig); } feature = Commands_Feature(GetEnabledInstance()); - if ( !feature.emergencyEnabledMutate - && !feature.IsUsingMutateInput() && !feature.IsUsingChatInput()) - { + noWayToInputCommands = !feature.emergencyEnabledMutate + &&!feature.IsUsingMutateInput() + && !feature.IsUsingChatInput(); + if (noWayToInputCommands) { default.emergencyEnabledMutate = true; feature.emergencyEnabledMutate = true; __server().unreal.mutator.OnMutate(feature).connect = HandleMutate; } } -/** - * Checks if `Commands_Feature` currently uses chat as input. - * If `Commands_Feature` is not enabled, then it does not use anything - * as input. - * - * @return `true` if `Commands_Feature` is currently enabled and is using chat - * as input and `false` otherwise. - */ -public final static function bool IsUsingChatInput() -{ +/// Checks if `Commands_Feature` currently uses chat as input. +/// +/// If `Commands_Feature` is not enabled, then it does not use anything +/// as input. +public final static function bool IsUsingChatInput() { local Commands_Feature instance; instance = Commands_Feature(GetEnabledInstance()); @@ -214,16 +262,11 @@ public final static function bool IsUsingChatInput() return false; } -/** - * Checks if `Commands_Feature` currently uses mutate command as input. - * If `Commands_Feature` is not enabled, then it does not use anything - * as input. - * - * @return `true` if `Commands_Feature` is currently enabled and is using - * mutate command as input and `false` otherwise. - */ -public final static function bool IsUsingMutateInput() -{ +/// Checks if `Commands_Feature` currently uses mutate command as input. +/// +/// If `Commands_Feature` is not enabled, then it does not use anything +/// as input. +public final static function bool IsUsingMutateInput() { local Commands_Feature instance; instance = Commands_Feature(GetEnabledInstance()); @@ -233,15 +276,10 @@ public final static function bool IsUsingMutateInput() return false; } -/** - * Returns prefix that will indicate that chat message is intended to be - * a command. By default "!". - * - * @return Prefix that indicates that chat message is intended to be a command. - * If `Commands_Feature` is disabled, always returns `false`. - */ -public final static function Text GetChatPrefix() -{ +/// Returns prefix that will indicate that chat message is intended to be a command. By default "!". +/// +/// If `Commands_Feature` is disabled, always returns `none`. +public final static function Text GetChatPrefix() { local Commands_Feature instance; instance = Commands_Feature(GetEnabledInstance()); @@ -251,31 +289,29 @@ public final static function Text GetChatPrefix() return none; } -/** - * Registers given command class, making it available for usage. - * - * If `commandClass` provides command with a name that is already taken - * (comparison is case-insensitive) by a different command - a warning will be - * logged and newly passed `commandClass` discarded. - * - * @param commandClass New command class to register. - */ -public final function RegisterCommand(class commandClass) -{ - local Text commandName, groupName; +/// Registers given command class, making it available. +/// +/// # Errors +/// +/// Returns `true` if command was successfully registered and `false` otherwise`. +/// +/// If `commandClass` provides command with a name that is already taken +/// (comparison is case-insensitive) by a different command - a warning will be +/// logged and newly passed `commandClass` discarded. +public final function bool RegisterCommand(class commandClass) { + local Text commandName, groupName; local ArrayList groupArray; - local Command newCommandInstance, existingCommandInstance; + local Command newCommandInstance, existingCommandInstance; - if (commandClass == none) return; - if (registeredCommands == none) return; + if (commandClass == none) return false; + if (registeredCommands == none) return false; - newCommandInstance = Command(_.memory.Allocate(commandClass, true)); - commandName = newCommandInstance.GetName(); - groupName = newCommandInstance.GetGroupName(); + 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) - { + if (existingCommandInstance != none) { _.logger.Auto(errCommandDuplicate) .ArgClass(existingCommandInstance.class) .Arg(commandName) @@ -283,7 +319,7 @@ public final function RegisterCommand(class commandClass) _.memory.Free(groupName); _.memory.Free(newCommandInstance); _.memory.Free(existingCommandInstance); - return; + return false; } // Otherwise record new command // `commandName` used as a key, do not deallocate it @@ -295,43 +331,31 @@ public final function RegisterCommand(class commandClass) } groupArray.AddItem(newCommandInstance); groupedCommands.SetItem(groupName, groupArray); - _.memory.Free(groupArray); - _.memory.Free(groupName); - _.memory.Free(commandName); - _.memory.Free(newCommandInstance); + _.memory.Free4(groupArray, groupName, commandName, newCommandInstance); + return true; } -/** - * Removes command of class `commandClass` from the list of - * registered commands. - * - * WARNING: removing once registered commands is not an action that is expected - * to be performed under normal circumstances and it is not efficient. - * It is linear on the current amount of commands. - * - * @param commandClass Class of command to remove from being registered. - */ -public final function RemoveCommand(class commandClass) -{ - local int i; - local CollectionIterator iter; - local Command nextCommand; - local Text nextCommandName; - local array commandGroup; - local array keysToRemove; +/// Removes command of given class from the list of registered commands. +/// +/// Removing once registered commands is not an action that is expected to be performed under normal +/// circumstances and it is not efficient. +/// It is linear on the current amount of commands. +public final function RemoveCommand(class commandClass) { + local int i; + local CollectionIterator iter; + local Command nextCommand; + local Text nextCommandName; + local array commandGroup; + local array keysToRemove; if (commandClass == none) return; if (registeredCommands == none) return; - for (iter = registeredCommands.Iterate(); !iter.HasFinished(); iter.Next()) - { + for (iter = registeredCommands.Iterate(); !iter.HasFinished(); iter.Next()) { nextCommand = Command(iter.Get()); nextCommandName = Text(iter.GetKey()); - if ( nextCommand == none || nextCommandName == none - || nextCommand.class != commandClass) - { - _.memory.Free(nextCommand); - _.memory.Free(nextCommandName); + if (nextCommand == none || nextCommandName == none || nextCommand.class != commandClass) { + _.memory.Free2(nextCommand, nextCommandName); continue; } keysToRemove[keysToRemove.length] = nextCommandName; @@ -339,57 +363,22 @@ public final function RemoveCommand(class commandClass) _.memory.Free(nextCommand); } iter.FreeSelf(); - for (i = 0; i < keysToRemove.length; i += 1) - { + for (i = 0; i < keysToRemove.length; i += 1) { 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 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); -} - -/** - * Returns command based on a given name. - * - * @param commandName Name of the registered `Command` to return. - * Case-insensitive. - * @return Command, registered with a given name `commandName`. - * If no command with such name was registered - returns `none`. - */ -public final function Command GetCommand(BaseText commandName) -{ +/// Returns command based on a given name. +/// +/// Name of the registered `Command` to return is case-insensitive. +/// +/// If no command with such name was registered - returns `none`. +public final function Command GetCommand(BaseText commandName) { local Text commandNameLowerCase; local Command commandInstance; @@ -402,13 +391,8 @@ public final function Command GetCommand(BaseText commandName) return commandInstance; } -/** - * Returns array of names of all available commands. - * - * @return Array of names of all available (registered) commands. - */ -public final function array GetCommandNames() -{ +/// Returns array of names of all available commands. +public final function array GetCommandNames() { local array emptyResult; if (registeredCommands != none) { @@ -417,26 +401,18 @@ public final function array 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 GetCommandNamesInGroup(BaseText groupName) -{ - local int i; - local ArrayList groupArray; - local Command nextCommand; - local array result; +/// Returns array of names of all available commands belonging to the group [`groupName`]. +public final function array GetCommandNamesInGroup(BaseText groupName) { + local int i; + local ArrayList groupArray; + local Command nextCommand; + local array result; if (groupedCommands == none) return result; groupArray = groupedCommands.GetArrayList(groupName); if (groupArray == none) return result; - for (i = 0; i < groupArray.GetLength(); i += 1) - { + for (i = 0; i < groupArray.GetLength(); i += 1) { nextCommand = Command(groupArray.GetItem(i)); if (nextCommand != none) { result[result.length] = nextCommand.GetName(); @@ -446,13 +422,8 @@ public final function array GetCommandNamesInGroup(BaseText groupName) return result; } -/** - * Returns all available command groups' names. - * - * @return Array of all available command groups' names. - */ -public final function array GetGroupsNames() -{ +/// Returns all available command groups' names. +public final function array GetGroupsNames() { local array emptyResult; if (groupedCommands != none) { @@ -461,158 +432,75 @@ public final function array GetGroupsNames() return emptyResult; } -/** - * Handles user input: finds appropriate command and passes the rest of - * the arguments to it for further processing. - * - * @param input Test that contains user's command input. - * @param callerPlayer Player that caused this command call. - */ -public final function HandleInput(BaseText input, EPlayer callerPlayer) -{ +/// Executes command based on the input. +/// +/// Takes [`commandLine`] as input with command's call, finds appropriate registered command +/// instance and executes it with parameters specified in the [`commandLine`]. +/// +/// [`callerPlayer`] has to be specified and represents instigator of this command that will receive +/// appropriate result/error messages. +/// +/// Returns `true` iff command was successfully executed. +/// +/// # Errors +/// +/// Doesn't log any errors, but can complain about errors in name or parameters to +/// the [`callerPlayer`] +public final function bool HandleInput(BaseText input, EPlayer callerPlayer) { + local bool result; local Parser wrapper; if (input == none) { - return; + return false; } wrapper = input.Parse(); - HandleInputWith(wrapper, callerPlayer); + result = HandleInputWith(wrapper, callerPlayer); wrapper.FreeSelf(); + return result; } -/** - * Handles user input: finds appropriate command and passes the rest of - * the arguments to it for further processing. - * - * @param parser Parser filled with user input that is expected to - * contain command's name and it's parameters. - * @param callerPlayer Player that caused this command call. - */ -public final function HandleInputWith(Parser parser, EPlayer callerPlayer) -{ - local int i; - local bool foundID; - local string steamID; - local PlayerController controller; - local Command commandInstance; - local Command.CallData callData; - local CommandCallPair callPair; - - if (parser == none) return; - if (callerPlayer == none) return; - if (!parser.Ok()) return; - controller = callerPlayer.GetController(); - if (controller == none) return; - - steamID = controller.GetPlayerIDHash(); - for (i = 0; i < allowedPlayers.length; i += 1) - { - if (allowedPlayers[i] == steamID) - { - foundID = true; - break; - } - } - if (!foundID) { - return; - } +/// Executes command based on the input. +/// +/// Takes [`commandLine`] as input with command's call, finds appropriate registered command +/// instance and executes it with parameters specified in the [`commandLine`]. +/// +/// [`callerPlayer`] has to be specified and represents instigator of this command that will receive +/// appropriate result/error messages. +/// +/// Returns `true` iff command was successfully executed. +/// +/// # Errors +/// +/// Doesn't log any errors, but can complain about errors in name or parameters to +/// the [`callerPlayer`] +public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) { + local bool errorOccured; + local Command commandInstance; + local Command.CallData callData; + local CommandCallPair callPair; + + if (parser == none) return false; + if (callerPlayer == none) return false; + if (!parser.Ok()) return false; + callPair = ParseCommandCallPairWith(parser); commandInstance = GetCommand(callPair.commandName); - if ( commandInstance == none - && callerPlayer != none && callerPlayer.IsExistent()) - { + if (commandInstance == none && callerPlayer != none && callerPlayer.IsExistent()) { callerPlayer .BorrowConsole() .Flush() .Say(F("{$TextFailure Command not found!}")); } - if (parser.Ok() && commandInstance != none) - { - callData = commandInstance - .ParseInputWith(parser, callerPlayer, callPair.subCommandName); - commandInstance.Execute(callData, callerPlayer); + if (parser.Ok() && commandInstance != none) { + callData = commandInstance.ParseInputWith(parser, callerPlayer, callPair.subCommandName); + errorOccured = commandInstance.Execute(callData, callerPlayer); commandInstance.DeallocateCallData(callData); } - _.memory.Free(callPair.commandName); - _.memory.Free(callPair.subCommandName); + _.memory.Free2(callPair.commandName, callPair.subCommandName); + return errorOccured; } -// Parses command's name into `CommandCallPair` - sub-command is filled in case -// specified name is an alias with specified sub-command name. -private final function CommandCallPair ParseCommandCallPairWith(Parser parser) -{ - local Text resolvedValue; - local MutableText userSpecifiedName; - local CommandCallPair result; - local Text.Character dotCharacter; - - if (parser == none) return result; - if (!parser.Ok()) return result; - - parser.MUntilMany(userSpecifiedName, commandDelimiters, true, true); - resolvedValue = _.alias.ResolveCommand(userSpecifiedName); - // This isn't an alias - if (resolvedValue == none) - { - result.commandName = userSpecifiedName; - return result; - } - // It is an alias - parse it - dotCharacter = _.text.GetCharacter("."); - resolvedValue.Parse() - .MUntil(result.commandName, dotCharacter) - .MatchS(".") - .MUntil(result.subCommandName, dotCharacter) - .FreeSelf(); - if (result.subCommandName.IsEmpty()) - { - result.subCommandName.FreeSelf(); - result.subCommandName = none; - } - resolvedValue.FreeSelf(); - return result; -} - -private function bool HandleCommands( - EPlayer sender, - MutableText message, - bool teamMessage) -{ - local Parser parser; - - // We are only interested in messages that start with `chatCommandPrefix` - parser = _.text.Parse(message); - if (!parser.Match(chatCommandPrefix).Ok()) - { - parser.FreeSelf(); - return true; - } - // Pass input to command feature - HandleInputWith(parser, sender); - parser.FreeSelf(); - return false; -} - -private function HandleMutate(string command, PlayerController sendingPlayer) -{ - local Parser parser; - local EPlayer sender; - - // A lot of other mutators use these commands - if (command ~= "help") return; - if (command ~= "version") return; - if (command ~= "status") return; - if (command ~= "credits") return; - - parser = _.text.ParseString(command); - sender = _.players.FromController(sendingPlayer); - HandleInputWith(parser, sender); - sender.FreeSelf(); - parser.FreeSelf(); -} - -defaultproperties -{ +defaultproperties { configClass = class'Commands' errCommandDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Command `%3` with the same name will be ignored.") } \ No newline at end of file From 9265e97c59391cf3693f360a5fd842e70eb9c4dd Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 13 Mar 2023 18:42:30 +0700 Subject: [PATCH 06/17] Move `CommandAPI` into base API --- .../Commands/BuiltInCommands/ACommandHelp.uc | 0 .../API}/Commands/Command.uc | 0 .../API}/Commands/CommandAPI.uc | 0 .../API}/Commands/CommandDataBuilder.uc | 0 .../API}/Commands/CommandParser.uc | 0 .../API}/Commands/Commands.uc | 0 .../API}/Commands/Commands_Feature.uc | 22 ++++++++++++------- .../API}/Commands/PlayersParser.uc | 0 .../API}/Commands/Tests/MockCommandA.uc | 0 .../API}/Commands/Tests/MockCommandB.uc | 0 .../API}/Commands/Tests/TEST_Command.uc | 0 .../Commands/Tests/TEST_CommandDataBuilder.uc | 0 12 files changed, 14 insertions(+), 8 deletions(-) rename sources/{LevelAPI/Features => BaseAPI/API}/Commands/BuiltInCommands/ACommandHelp.uc (100%) rename sources/{LevelAPI/Features => BaseAPI/API}/Commands/Command.uc (100%) rename sources/{LevelAPI/Features => BaseAPI/API}/Commands/CommandAPI.uc (100%) rename sources/{LevelAPI/Features => BaseAPI/API}/Commands/CommandDataBuilder.uc (100%) rename sources/{LevelAPI/Features => BaseAPI/API}/Commands/CommandParser.uc (100%) rename sources/{LevelAPI/Features => BaseAPI/API}/Commands/Commands.uc (100%) rename sources/{LevelAPI/Features => BaseAPI/API}/Commands/Commands_Feature.uc (96%) rename sources/{LevelAPI/Features => BaseAPI/API}/Commands/PlayersParser.uc (100%) rename sources/{LevelAPI/Features => BaseAPI/API}/Commands/Tests/MockCommandA.uc (100%) rename sources/{LevelAPI/Features => BaseAPI/API}/Commands/Tests/MockCommandB.uc (100%) rename sources/{LevelAPI/Features => BaseAPI/API}/Commands/Tests/TEST_Command.uc (100%) rename sources/{LevelAPI/Features => BaseAPI/API}/Commands/Tests/TEST_CommandDataBuilder.uc (100%) diff --git a/sources/LevelAPI/Features/Commands/BuiltInCommands/ACommandHelp.uc b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandHelp.uc similarity index 100% rename from sources/LevelAPI/Features/Commands/BuiltInCommands/ACommandHelp.uc rename to sources/BaseAPI/API/Commands/BuiltInCommands/ACommandHelp.uc diff --git a/sources/LevelAPI/Features/Commands/Command.uc b/sources/BaseAPI/API/Commands/Command.uc similarity index 100% rename from sources/LevelAPI/Features/Commands/Command.uc rename to sources/BaseAPI/API/Commands/Command.uc diff --git a/sources/LevelAPI/Features/Commands/CommandAPI.uc b/sources/BaseAPI/API/Commands/CommandAPI.uc similarity index 100% rename from sources/LevelAPI/Features/Commands/CommandAPI.uc rename to sources/BaseAPI/API/Commands/CommandAPI.uc diff --git a/sources/LevelAPI/Features/Commands/CommandDataBuilder.uc b/sources/BaseAPI/API/Commands/CommandDataBuilder.uc similarity index 100% rename from sources/LevelAPI/Features/Commands/CommandDataBuilder.uc rename to sources/BaseAPI/API/Commands/CommandDataBuilder.uc diff --git a/sources/LevelAPI/Features/Commands/CommandParser.uc b/sources/BaseAPI/API/Commands/CommandParser.uc similarity index 100% rename from sources/LevelAPI/Features/Commands/CommandParser.uc rename to sources/BaseAPI/API/Commands/CommandParser.uc diff --git a/sources/LevelAPI/Features/Commands/Commands.uc b/sources/BaseAPI/API/Commands/Commands.uc similarity index 100% rename from sources/LevelAPI/Features/Commands/Commands.uc rename to sources/BaseAPI/API/Commands/Commands.uc diff --git a/sources/LevelAPI/Features/Commands/Commands_Feature.uc b/sources/BaseAPI/API/Commands/Commands_Feature.uc similarity index 96% rename from sources/LevelAPI/Features/Commands/Commands_Feature.uc rename to sources/BaseAPI/API/Commands/Commands_Feature.uc index 30c0ec3..f02ae16 100644 --- a/sources/LevelAPI/Features/Commands/Commands_Feature.uc +++ b/sources/BaseAPI/API/Commands/Commands_Feature.uc @@ -73,7 +73,7 @@ var private /*config*/ bool useMutateInput; // Default is "!". Empty values are also treated as "!". var private /*config*/ Text chatCommandPrefix; -var LoggerAPI.Definition errCommandDuplicate; +var LoggerAPI.Definition errCommandDuplicate, errServerAPIUnavailable; protected function OnEnabled() { registeredCommands = _.collections.EmptyHashTable(); @@ -95,10 +95,11 @@ protected function OnEnabled() { _.chat.OnMessage(self).Disconnect(); } if (useMutateInput || emergencyEnabledMutate) { - _server.unreal.mutator.OnMutate(self).connect = HandleMutate; - } - else { - _server.unreal.mutator.OnMutate(self).Disconnect(); + if (__server() != none) { + __server().unreal.mutator.OnMutate(self).connect = HandleMutate; + } else { + _.logger.Auto(errServerAPIUnavailable); + } } } @@ -106,8 +107,8 @@ protected function OnDisabled() { if (useChatInput) { _.chat.OnMessage(self).Disconnect(); } - if (useMutateInput) { - _server.unreal.mutator.OnMutate(self).Disconnect(); + if (useMutateInput && __server() != none) { + __server().unreal.mutator.OnMutate(self).Disconnect(); } useChatInput = false; useMutateInput = false; @@ -244,7 +245,11 @@ public final static function EmergencyEnable() { if (noWayToInputCommands) { default.emergencyEnabledMutate = true; feature.emergencyEnabledMutate = true; - __server().unreal.mutator.OnMutate(feature).connect = HandleMutate; + if (__server() != none) { + __server().unreal.mutator.OnMutate(feature).connect = HandleMutate; + } else { + __().logger.Auto(default.errServerAPIUnavailable); + } } } @@ -503,4 +508,5 @@ public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) defaultproperties { configClass = class'Commands' errCommandDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Command `%3` with the same name will be ignored.") + errServerAPIUnavailable = (l=LOG_Error,m="Server API is currently unavailable. Input methods through chat and mutate command will not be available.") } \ No newline at end of file diff --git a/sources/LevelAPI/Features/Commands/PlayersParser.uc b/sources/BaseAPI/API/Commands/PlayersParser.uc similarity index 100% rename from sources/LevelAPI/Features/Commands/PlayersParser.uc rename to sources/BaseAPI/API/Commands/PlayersParser.uc diff --git a/sources/LevelAPI/Features/Commands/Tests/MockCommandA.uc b/sources/BaseAPI/API/Commands/Tests/MockCommandA.uc similarity index 100% rename from sources/LevelAPI/Features/Commands/Tests/MockCommandA.uc rename to sources/BaseAPI/API/Commands/Tests/MockCommandA.uc diff --git a/sources/LevelAPI/Features/Commands/Tests/MockCommandB.uc b/sources/BaseAPI/API/Commands/Tests/MockCommandB.uc similarity index 100% rename from sources/LevelAPI/Features/Commands/Tests/MockCommandB.uc rename to sources/BaseAPI/API/Commands/Tests/MockCommandB.uc diff --git a/sources/LevelAPI/Features/Commands/Tests/TEST_Command.uc b/sources/BaseAPI/API/Commands/Tests/TEST_Command.uc similarity index 100% rename from sources/LevelAPI/Features/Commands/Tests/TEST_Command.uc rename to sources/BaseAPI/API/Commands/Tests/TEST_Command.uc diff --git a/sources/LevelAPI/Features/Commands/Tests/TEST_CommandDataBuilder.uc b/sources/BaseAPI/API/Commands/Tests/TEST_CommandDataBuilder.uc similarity index 100% rename from sources/LevelAPI/Features/Commands/Tests/TEST_CommandDataBuilder.uc rename to sources/BaseAPI/API/Commands/Tests/TEST_CommandDataBuilder.uc From 3d7f11688cde246cad52ce0b0cdddb08d975eb0b Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 13 Mar 2023 19:12:05 +0700 Subject: [PATCH 07/17] Add async methods for registering commands --- sources/BaseAPI/API/Commands/CommandAPI.uc | 46 +++++++++++++++- .../API/Commands/CommandRegistrationJob.uc | 54 +++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 sources/BaseAPI/API/Commands/CommandRegistrationJob.uc diff --git a/sources/BaseAPI/API/Commands/CommandAPI.uc b/sources/BaseAPI/API/Commands/CommandAPI.uc index 8b46965..e31d2ec 100644 --- a/sources/BaseAPI/API/Commands/CommandAPI.uc +++ b/sources/BaseAPI/API/Commands/CommandAPI.uc @@ -21,11 +21,27 @@ */ class CommandAPI extends AcediaObject; +// Classes registered to be registered in async way +var private array< class > pendingClasses; +// Job that is supposed to register pending commands +var private CommandRegistrationJob registeringJob; + var private Commands_Feature commandsFeature; // DO NOT CALL MANUALLY -public final /*internal*/ function _reloadFeature() -{ +public final /*internal*/ function class _popPending() { + local class result; + + if (pendingClasses.length == 0) { + return none; + } + result = pendingClasses[0]; + pendingClasses.Remove(0, 1); + return result; +} + +// DO NOT CALL MANUALLY +public final /*internal*/ function _reloadFeature() { if (commandsFeature != none) { commandsFeature.FreeSelf(); commandsFeature = none; @@ -41,6 +57,8 @@ public final function bool AreCommandsEnabled() { /// Registers given command class, making it available via `Execute()`. /// +/// Unless you need command right now, it is recommended to use `RegisterCommandAsync()` instead. +/// /// Returns `true` if command was successfully registered and `false` otherwise`. /// /// # Errors @@ -55,6 +73,30 @@ public final function bool RegisterCommand(class commandClass) { return false; } +/// Registers given command class asynchronously, making it available via `Execute()`. +/// +/// Doesn't register commands immediately, instead scheduling it to be done at a later moment in +/// time, allowing. +/// This can help to reduce amount of work we do every tick during server startup, therefore +/// avoiding crashed due to the faulty infinite loop detection. +/// +/// # Errors +/// +/// If `commandClass` provides command with a name that is already taken (comparison is +/// case-insensitive) by a different command - a warning will be logged and newly passed +/// `commandClass` discarded. +public final function RegisterCommandAsync(class commandClass) { + if (commandsFeature == none) { + return; + } + pendingClasses[pendingClasses.length] = commandClass; + if (registeringJob == none || registeringJob.IsCompleted()) { + _.memory.Free(registeringJob); + registeringJob = CommandRegistrationJob(_.memory.Allocate(class'CommandRegistrationJob')); + _.scheduler.AddJob(registeringJob); + } +} + /// Removes command of given class from the list of registered commands. /// /// Removing once registered commands is not an action that is expected to be performed under normal diff --git a/sources/BaseAPI/API/Commands/CommandRegistrationJob.uc b/sources/BaseAPI/API/Commands/CommandRegistrationJob.uc new file mode 100644 index 0000000..5bc783c --- /dev/null +++ b/sources/BaseAPI/API/Commands/CommandRegistrationJob.uc @@ -0,0 +1,54 @@ +/** + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 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 CommandRegistrationJob extends SchedulerJob; + +var private class nextCommand; + +protected function Constructor() { + nextCommand = _.commands._popPending(); +} + +protected function Finalizer() { + nextCommand = none; +} + +public function bool IsCompleted() { + return (nextCommand == none); +} + +public function DoWork(int allottedWorkUnits) { + local int i, iterationsAmount; + + // Expected 300 units per tick, to register 20 commands per tick use about 10 + iterationsAmount = Max(allottedWorkUnits / 10, 1); + for (i = 0; i < iterationsAmount; i += 1) { + _.commands.RegisterCommand(nextCommand); + nextCommand = _.commands._popPending(); + if (nextCommand == none) { + break; + } + } +} + +defaultproperties +{ +} \ No newline at end of file From c4247a67d0efa63afd7e563732898f7803bac93f Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 13 Mar 2023 19:40:46 +0700 Subject: [PATCH 08/17] Fix style for `CommandParser` --- sources/BaseAPI/API/Commands/CommandParser.uc | 956 ++++++++---------- 1 file changed, 442 insertions(+), 514 deletions(-) diff --git a/sources/BaseAPI/API/Commands/CommandParser.uc b/sources/BaseAPI/API/Commands/CommandParser.uc index afb1c70..57d58c1 100644 --- a/sources/BaseAPI/API/Commands/CommandParser.uc +++ b/sources/BaseAPI/API/Commands/CommandParser.uc @@ -1,9 +1,8 @@ /** - * Auxiliary class for parsing user's input into a `Command.CallData` based on - * a given `Command.Data`. While it's meant to be allocated for - * a `self.ParseWith()` call and deallocated right after, it can be reused - * without deallocation. - * Copyright 2021-2022 Anton Tarasenko + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -23,168 +22,159 @@ class CommandParser extends AcediaObject dependson(Command); -/** - * # `CommandParser` - * - * Class specialized for parsing user input of the command's call into - * `Command.CallData` structure with the information about all parsed - * arguments. - * `CommandParser` is not made to parse the whole input: - * - * * Command's name needs to be parsed and resolved as an alias before - * using this parser - it won't do this hob for you; - * * List of targeted players must also be parsed using `PlayersParser` - - * `CommandParser` won't do this for you; - * * Optionally one can also decide on the referred subcommand and pass it - * into `ParseWith()` method. If subcommand's name is not passed - - * `CommandParser` will try to parse it itself. This feature is used to - * add support for subcommand aliases. - * - * However, above steps are handled by `Commands_Feature` and one only needs to - * call that feature's `HandleInput()` methods to pass user input with command - * call line there. - * - * ## Usage - * - * Allocate `CommandParser` and call `ParseWith()` method, providing it with: - * - * 1. `Parser`, filled with command call input; - * 2. Command's data that describes subcommands, options and their - * parameters for the command, which call we are parsing; - * 3. (Optionally) `EPlayer` reference to the player that initiated - * the command call; - * 4. (Optionally) Subcommand to be used - this will prevent - * `CommandParser` from parsing subcommand name itself. Used for - * implementing aliases that refer to a particular subcommand. - * - * ## Implementation - * - * `CommandParser` stores both its state and command data, relevant to - * parsing, as its member variables during the whole parsing process, - * instead of passing that data around in every single method. - * - * We will give a brief overview of how around 20 parsing methods below - * are interconnected. - * The only public method `ParseWith()` is used to start parsing and it - * uses `PickSubCommand()` to first try and figure out what sub command is - * intended by user's input. - * Main bulk of the work is done by `ParseParameterArrays()` method, - * for simplicity broken into two `ParseRequiredParameterArray()` and - * `ParseOptionalParameterArray()` methods that can parse parameters for both - * command itself and it's options. - * They go through arrays of required and optional parameters, - * calling `ParseParameter()` for each parameters, which in turn can make - * several calls of `ParseSingleValue()` to parse parameters' values: - * it is called once for single-valued parameters, but possibly several times - * for list parameters that can contain several values. - * So main parsing method looks something like: - * - * ParseParameterArrays() { - * loop ParseParameter() { - * loop ParseSingleValue() - * } - * } - * `ParseSingleValue()` is essentially that redirects it's method call to - * another, more specific, parsing method based on the parameter type. - * - * Finally, to allow users to specify options at any point in command, - * we call `TryParsingOptions()` at the beginning of every - * `ParseSingleValue()` (the only parameter that has higher priority than - * options is `CPT_Remainder`), since option definition can appear at any place - * between parameters. We also call `TryParsingOptions()` *after* we've parsed - * all command's parameters, since that case won't be detected by parsing - * them *before* every parameter. - * `TryParsingOptions()` itself simply tries to detect "-" and "--" - * prefixes (filtering out negative numeric values) and then redirect the call - * to either of more specialized methods: `ParseLongOption()` or - * `ParseShortOption()`, that can in turn make another `ParseParameterArrays()` - * call, if specified option has parameters. - * NOTE: `ParseParameterArrays()` can only nest in itself once, since - * option declaration always interrupts previous option's parameter list. - * Rest of the methods perform simple auxiliary functions. - */ -// Parser filled with user input. -var private Parser commandParser; -// Data for sub-command specified by both command we are parsing -// and user's input; determined early during parsing. -var private Command.SubCommand pickedSubCommand; -// Options available for the command we are parsing. -var private array availableOptions; -// Result variable we are filling during the parsing process, -// should be `none` outside of `self.ParseWith()` method call. -var private Command.CallData nextResult; - -// Describes which parameters we are currently parsing, classifying them -// as either "necessary" or "extra". -// E.g. if last require parameter is a list of integers, -// then after parsing first integer we are: -// * Still parsing required *parameter* "integer list"; -// * But no more integers are *necessary* for successful parsing. +//! Class specialized for parsing user input of the command's call into `Command.CallData` structure +//! with the information about all parsed arguments. +//! +//! `CommandParser` is not made to parse the whole input: +//! +//! * Command's name needs to be parsed and resolved as an alias before using this parser - +//! it won't do this hob for you; +//! * List of targeted players must also be parsed using `PlayersParser` - `CommandParser` won't do +//! this for you; +//! * Optionally one can also decide on the referred subcommand and pass it into `ParseWith()` +//! method. If subcommand's name is not passed - `CommandParser` will try to parse it itself. +//! This feature is used to add support for subcommand aliases. +//! +//! However, above steps are handled by `Commands_Feature` and one only needs to call that feature's +//! `HandleInput()` methods to pass user input with command call line there. +//! +//! # Usage +//! +//! Allocate `CommandParser` and call `ParseWith()` method, providing it with: +//! +//! 1. `Parser`, filled with command call input; +//! 2. Command's data that describes subcommands, options and their parameters for the command, +//! which call we are parsing; +//! 3. (Optionally) `EPlayer` reference to the player that initiated the command call; +//! 4. (Optionally) Subcommand to be used - this will prevent `CommandParser` from parsing +//! subcommand name itself. Used for implementing aliases that refer to a particular subcommand. +//! +//! # Implementation +//! +//! `CommandParser` stores both its state and command data, relevant to parsing, as its member +//! variables during the whole parsing process, instead of passing that data around in every single +//! method. +//! +//! We will give a brief overview of how around 20 parsing methods below are interconnected. +//! +//! The only public method `ParseWith()` is used to start parsing and it uses `PickSubCommand()` to +//! first try and figure out what sub command is intended by user's input. +//! +//! Main bulk of the work is done by `ParseParameterArrays()` method, for simplicity broken into two +//! `ParseRequiredParameterArray()` and `ParseOptionalParameterArray()` methods that can parse +//! parameters for both command itself and it's options. +//! +//! They go through arrays of required and optional parameters, calling `ParseParameter()` for each +//! parameters, which in turn can make several calls of `ParseSingleValue()` to parse parameters' +//! values: it is called once for single-valued parameters, but possibly several times for list +//! parameters that can contain several values. +//! +//! So main parsing method looks something like: +//! +//! ``` +//! ParseParameterArrays() { +//! loop ParseParameter() { +//! loop ParseSingleValue() +//! } +//! } +//! ``` +//! +//! `ParseSingleValue()` is essentially that redirects it's method call to another, more specific, +//! parsing method based on the parameter type. +//! +//! Finally, to allow users to specify options at any point in command, we call +//! `TryParsingOptions()` at the beginning of every `ParseSingleValue()` (the only parameter that +//! has higher priority than options is `CPT_Remainder`), since option definition can appear at any +//! place between parameters. We also call `TryParsingOptions()` *after* we've parsed all command's +//! parameters, since that case won't be detected by parsing them *before* every parameter. +//! +//! `TryParsingOptions()` itself simply tries to detect "-" and "--" prefixes (filtering out +//! negative numeric values) and then redirect the call to either of more specialized methods: +//! `ParseLongOption()` or `ParseShortOption()`, that can in turn make another +//! `ParseParameterArrays()` call, if specified option has parameters. +//! +//! NOTE: `ParseParameterArrays()` can only nest in itself once, since option declaration always +//! interrupts previous option's parameter list. +//! Rest of the methods perform simple auxiliary functions. + +// Describes which parameters we are currently parsing, classifying them +// as either "necessary" or "extra". // -// Therefore we consider parameter "necessary" if the lack of it will -// result in failed parsing and "extra" otherwise. -enum ParsingTarget -{ - // We are in the process of parsing required parameters, that must all - // be present. - // This case does not include parsing last required parameter: it needs - // to be treated differently to track when we change from "necessary" to - // "extra" parameters. +// E.g. if last require parameter is a list of integers, +// then after parsing first integer we are: +// +// * Still parsing required *parameter* "integer list"; +// * But no more integers are *necessary* for successful parsing. +// +// Therefore we consider parameter "necessary" if the lack of it will\ +// result in failed parsing and "extra" otherwise. +enum ParsingTarget { + // We are in the process of parsing required parameters, that must all + // be present. + // This case does not include parsing last required parameter: it needs + // to be treated differently to track when we change from "necessary" to + // "extra" parameters. CPT_NecessaryParameter, - // We are parsing last necessary parameter. + // We are parsing last necessary parameter. CPT_LastNecessaryParameter, - // We are not parsing extra parameters that can be safely omitted. + // We are not parsing extra parameters that can be safely omitted. CPT_ExtraParameter, }; -// Parser for player parameters, setup with a caller for current parsing +// Parser filled with user input. +var private Parser commandParser; +// Data for sub-command specified by both command we are parsing +// and user's input; determined early during parsing. +var private Command.SubCommand pickedSubCommand; +// Options available for the command we are parsing. +var private array availableOptions; +// Result variable we are filling during the parsing process, +// should be `none` outside of `self.ParseWith()` method call. +var private Command.CallData nextResult; + +// Parser for player parameters, setup with a caller for current parsing var private PlayersParser currentPlayersParser; -// Current `ParsingTarget`, see it's enum description for more details +// Current `ParsingTarget`, see it's enum description for more details var private ParsingTarget currentTarget; -// `true` means we are parsing parameters for a command's option and -// `false` means we are parsing command's own parameters +// `true` means we are parsing parameters for a command's option and +// `false` means we are parsing command's own parameters var private bool currentTargetIsOption; -// If we are parsing parameters for an option (`currentTargetIsOption == true`) -// this variable will store that option's data. +// If we are parsing parameters for an option (`currentTargetIsOption == true`) +// this variable will store that option's data. var private Command.Option targetOption; -// Last successful state of `commandParser`. +// Last successful state of `commandParser`. var Parser.ParserState confirmedState; -// Options we have so far encountered during parsing, necessary since we want -// to forbid specifying th same option more than once. +// Options we have so far encountered during parsing, necessary since we want +// to forbid specifying th same option more than once. var private array usedOptions; -// Literals that can be used as boolean values +// Literals that can be used as boolean values var private array booleanTrueEquivalents; var private array booleanFalseEquivalents; var LoggerAPI.Definition errNoSubCommands; -protected function Finalizer() -{ +protected function Finalizer() { Reset(); } -// Zero important variables -private final function Reset() -{ +// Zero important variables +private final function Reset() { local Command.CallData blankCallData; _.memory.Free(currentPlayersParser); - currentPlayersParser = none; - // We didn't create this one and are not meant to free it either - commandParser = none; - nextResult = blankCallData; - currentTarget = CPT_NecessaryParameter; - currentTargetIsOption = false; - usedOptions.length = 0; + currentPlayersParser = none; + // We didn't create this one and are not meant to free it either + commandParser = none; + nextResult = blankCallData; + currentTarget = CPT_NecessaryParameter; + currentTargetIsOption = false; + usedOptions.length = 0; } -// Auxiliary method for recording errors -private final function DeclareError( - Command.ErrorType type, - optional BaseText cause) -{ +// Auxiliary method for recording errors +private final function DeclareError(Command.ErrorType type, optional BaseText cause) { nextResult.parsingError = type; if (cause != none) { nextResult.errorCause = cause.Copy(); @@ -194,95 +184,78 @@ private final function DeclareError( } } -// Assumes `commandParser != none`, is in successful state. -// Picks a sub command based on it's contents (parser's pointer must be -// before where subcommand's name is specified). -// If `specifiedSubCommand` is not `none` - will always use that value -// instead of parsing it from `commandParser`. -private final function PickSubCommand( - Command.Data commandData, - BaseText specifiedSubCommand) -{ - local int i; - local MutableText candidateSubCommandName; - local Command.SubCommand emptySubCommand; +// Assumes `commandParser != none`, is in successful state. +// +// Picks a sub command based on it's contents (parser's pointer must be before where subcommand's +// name is specified). +// +// If `specifiedSubCommand` is not `none` - will always use that value instead of parsing it from +// `commandParser`. +private final function PickSubCommand(Command.Data commandData, BaseText specifiedSubCommand) { + local int i; + local MutableText candidateSubCommandName; + local Command.SubCommand emptySubCommand; local array allSubCommands; allSubCommands = commandData.subCommands; - if (allSubcommands.length == 0) - { + if (allSubcommands.length == 0) { _.logger.Auto(errNoSubCommands).ArgClass(class); pickedSubCommand = emptySubCommand; return; } - // Get candidate name + // Get candidate name confirmedState = commandParser.GetCurrentState(); if (specifiedSubCommand != none) { candidateSubCommandName = specifiedSubCommand.MutableCopy(); - } - else { + } else { commandParser.Skip().MUntil(candidateSubCommandName,, true); } - // Try matching it to sub commands + // Try matching it to sub commands pickedSubCommand = allSubcommands[0]; - if (candidateSubCommandName.IsEmpty()) - { + if (candidateSubCommandName.IsEmpty()) { candidateSubCommandName.FreeSelf(); return; } - for (i = 0; i < allSubcommands.length; i += 1) - { - if (candidateSubCommandName.Compare(allSubcommands[i].name)) - { + for (i = 0; i < allSubcommands.length; i += 1) { + if (candidateSubCommandName.Compare(allSubcommands[i].name)) { candidateSubCommandName.FreeSelf(); pickedSubCommand = allSubcommands[i]; return; } } - // We will only reach here if we did not match any sub commands, - // meaning that whatever consumed by `candidateSubCommandName` probably - // has a different meaning. + // We will only reach here if we did not match any sub commands, + // meaning that whatever consumed by `candidateSubCommandName` probably + // has a different meaning. commandParser.RestoreState(confirmedState); } -/** - * Parses user's input given in `parser` using command's information given by - * `commandData`. - * - * @param parser `Parser`, initialized with user's input that will - * need to be parsed as a command's call. - * @param commandData Describes what parameters and options should be - * expected in user's input. `Text` values from `commandData` can be used - * inside resulting `Command.CallData`, so deallocating them can - * invalidate returned value. - * @param callerPlayer Player that called this command, if applicable. - * @param specifiedSubCommand Optionally, sub-command can be specified for - * the `CommandParser` to use. If this argument's value is `none` - it will - * be parsed from `parser`'s data instead. - * @return Results of parsing, described by `Command.CallData`. - * Returned object is guaranteed to be not `none`. - */ +/// Parses user's input given in [`parser`] using command's information given by [`commandData`]. +/// +/// Optionally, sub-command can be specified for the [`CommandParser`] to use via +/// [`specifiedSubCommand`] argument. +/// If this argument's value is `none` - it will be parsed from `parser`'s data instead. +/// +/// Returns results of parsing, described by `Command.CallData`. +/// Returned object is guaranteed to be not `none`. public final function Command.CallData ParseWith( - Parser parser, - Command.Data commandData, - optional EPlayer callerPlayer, - optional BaseText specifiedSubCommand) -{ - local HashTable commandParameters; - // Temporary object to return `nextResult` while setting variable to `none` - local Command.CallData toReturn; - - nextResult.parameters = _.collections.EmptyHashTable(); - nextResult.options = _.collections.EmptyHashTable(); - if (commandData.subCommands.length == 0) - { + Parser parser, + Command.Data commandData, + EPlayer callerPlayer, + optional BaseText specifiedSubCommand +) { + local HashTable commandParameters; + // Temporary object to return `nextResult` while setting variable to `none` + local Command.CallData toReturn; + + nextResult.parameters = _.collections.EmptyHashTable(); + nextResult.options = _.collections.EmptyHashTable(); + if (commandData.subCommands.length == 0) { DeclareError(CET_NoSubCommands, none); toReturn = nextResult; Reset(); return toReturn; } - if (parser == none || !parser.Ok()) - { + if (parser == none || !parser.Ok()) { DeclareError(CET_BadParser, none); toReturn = nextResult; Reset(); @@ -293,28 +266,25 @@ public final function Command.CallData ParseWith( currentPlayersParser = PlayersParser(_.memory.Allocate(class'PlayersParser')); currentPlayersParser.SetSelf(callerPlayer); - // (subcommand) (parameters, possibly with options) and nothing else! + // (subcommand) (parameters, possibly with options) and nothing else! PickSubCommand(commandData, specifiedSubCommand); nextResult.subCommandName = pickedSubCommand.name.Copy(); - commandParameters = ParseParameterArrays( pickedSubCommand.required, - pickedSubCommand.optional); - AssertNoTrailingInput(); // make sure there is nothing else + commandParameters = ParseParameterArrays(pickedSubCommand.required, pickedSubCommand.optional); + AssertNoTrailingInput(); // make sure there is nothing else if (commandParser.Ok()) { nextResult.parameters = commandParameters; - } - else { + } else { _.memory.Free(commandParameters); } - // Clean up + // Clean up toReturn = nextResult; Reset(); return toReturn; } -// Assumes `commandParser` is not `none` -// Declares an error if `commandParser` still has any input left -private final function AssertNoTrailingInput() -{ +// Assumes `commandParser` is not `none` +// Declares an error if `commandParser` still has any input left +private final function AssertNoTrailingInput() { local Text remainder; if (!commandParser.Ok()) return; @@ -325,58 +295,53 @@ private final function AssertNoTrailingInput() remainder.FreeSelf(); } -// Assumes `commandParser` is not `none`. -// Parses given required and optional parameters along with any -// possible option declarations. -// Returns `HashTable` filled with (variable, parsed value) pairs. -// Failure is equal to `commandParser` entering into a failed state. +// Assumes `commandParser` is not `none`. +// Parses given required and optional parameters along with any possible option declarations. +// Returns `HashTable` filled with (variable, parsed value) pairs. +// Failure is equal to `commandParser` entering into a failed state. private final function HashTable ParseParameterArrays( array requiredParameters, - array optionalParameters) -{ + array optionalParameters +) { local HashTable parsedParameters; if (!commandParser.Ok()) { return none; } parsedParameters = _.collections.EmptyHashTable(); - // Parse parameters + // Parse parameters ParseRequiredParameterArray(parsedParameters, requiredParameters); ParseOptionalParameterArray(parsedParameters, optionalParameters); - // Parse trailing options + // Parse trailing options while (TryParsingOptions()); return parsedParameters; } -// Assumes `commandParser` and `parsedParameters` are not `none`. -// Parses given required parameters along with any possible option -// declarations into given `parsedParameters` `HashTable`. +// Assumes `commandParser` and `parsedParameters` are not `none`. +// +// Parses given required parameters along with any possible option declarations into given +// `parsedParameters` `HashTable`. private final function ParseRequiredParameterArray( HashTable parsedParameters, - array requiredParameters) -{ + array requiredParameters +) { local int i; if (!commandParser.Ok()) { return; } currentTarget = CPT_NecessaryParameter; - while (i < requiredParameters.length) - { + while (i < requiredParameters.length) { if (i == requiredParameters.length - 1) { currentTarget = CPT_LastNecessaryParameter; } - // Parse parameters one-by-one, reporting appropriate errors - if (!ParseParameter(parsedParameters, requiredParameters[i])) - { - // Any failure to parse required parameter leads to error - if (currentTargetIsOption) - { + // Parse parameters one-by-one, reporting appropriate errors + if (!ParseParameter(parsedParameters, requiredParameters[i])) { + // Any failure to parse required parameter leads to error + if (currentTargetIsOption) { DeclareError( CET_NoRequiredParamForOption, targetOption.longName); - } - else - { + } else { DeclareError( CET_NoRequiredParam, requiredParameters[i].displayName); } @@ -387,30 +352,29 @@ private final function ParseRequiredParameterArray( currentTarget = CPT_ExtraParameter; } -// Assumes `commandParser` and `parsedParameters` are not `none`. -// Parses given optional parameters along with any possible option -// declarations into given `parsedParameters` hash table. +// Assumes `commandParser` and `parsedParameters` are not `none`. +// +// Parses given optional parameters along with any possible option declarations into given +// `parsedParameters` hash table. private final function ParseOptionalParameterArray( - HashTable parsedParameters, - array optionalParameters) -{ + HashTable parsedParameters, + array optionalParameters +) { local int i; if (!commandParser.Ok()) { return; } - while (i < optionalParameters.length) - { + while (i < optionalParameters.length) { confirmedState = commandParser.GetCurrentState(); - // Parse parameters one-by-one, reporting appropriate errors - if (!ParseParameter(parsedParameters, optionalParameters[i])) - { - // Propagate errors + // Parse parameters one-by-one, reporting appropriate errors + if (!ParseParameter(parsedParameters, optionalParameters[i])) { + // Propagate errors if (nextResult.parsingError != CET_None) { return; } - // Failure to parse optional parameter is fine if - // it is caused by that parameters simply missing + // Failure to parse optional parameter is fine if + // it is caused by that parameters simply missing commandParser.RestoreState(confirmedState); break; } @@ -418,26 +382,26 @@ private final function ParseOptionalParameterArray( } } -// Assumes `commandParser` and `parsedParameters` are not `none`. -// Parses one given parameter along with any possible option -// declarations into given `parsedParameters` `HashTable`. -// Returns `true` if we've successfully parsed given parameter without -// any errors. +// Assumes `commandParser` and `parsedParameters` are not `none`. +// +// Parses one given parameter along with any possible option declarations into given +// `parsedParameters` `HashTable`. +// +// Returns `true` if we've successfully parsed given parameter without any errors. private final function bool ParseParameter( - HashTable parsedParameters, - Command.Parameter expectedParameter) -{ + HashTable parsedParameters, + Command.Parameter expectedParameter +) { local bool parsedEnough; confirmedState = commandParser.GetCurrentState(); - while (ParseSingleValue(parsedParameters, expectedParameter)) - { + while (ParseSingleValue(parsedParameters, expectedParameter)) { if (currentTarget == CPT_LastNecessaryParameter) { currentTarget = CPT_ExtraParameter; } parsedEnough = true; - // We are done if there is either no more input or we only needed - // to parse a single value + // We are done if there is either no more input or we only needed + // to parse a single value if (!expectedParameter.allowsList) { return true; } @@ -446,196 +410,173 @@ private final function bool ParseParameter( } confirmedState = commandParser.GetCurrentState(); } - // We only succeeded in parsing if we've parsed enough for - // a given parameter and did not encounter any errors + // We only succeeded in parsing if we've parsed enough for + // a given parameter and did not encounter any errors if (parsedEnough && nextResult.parsingError == CET_None) { commandParser.RestoreState(confirmedState); return true; } - // Clean up any values `ParseSingleValue` might have recorded + // Clean up any values `ParseSingleValue` might have recorded parsedParameters.RemoveItem(expectedParameter.variableName); return false; } -// Assumes `commandParser` and `parsedParameters` are not `none`. -// Parses a single value for a given parameter (e.g. one integer for -// integer or integer list parameter types) along with any possible option -// declarations into given `parsedParameters` `HashTable`. -// Returns `true` if we've successfully parsed a single value without -// any errors. +// Assumes `commandParser` and `parsedParameters` are not `none`. +// +// Parses a single value for a given parameter (e.g. one integer for integer or integer list +// parameter types) along with any possible option declarations into given `parsedParameters`. +// +// Returns `true` if we've successfully parsed a single value without any errors. private final function bool ParseSingleValue( - HashTable parsedParameters, - Command.Parameter expectedParameter) -{ - // Before parsing any other value we need to check if user has - // specified any options instead. - // However this might lead to errors if we are already parsing - // necessary parameters of another option: - // we must handle such situation and report an error. - if (currentTargetIsOption) - { - // There is no problem is option's parameter is remainder + HashTable parsedParameters, + Command.Parameter expectedParameter +) { + // Before parsing any other value we need to check if user has specified any options instead. + // + // However this might lead to errors if we are already parsing necessary parameters of another + // option: we must handle such situation and report an error. + if (currentTargetIsOption) { + // There is no problem is option's parameter is remainder if (expectedParameter.type == CPT_Remainder) { return ParseRemainderValue(parsedParameters, expectedParameter); } - if (currentTarget != CPT_ExtraParameter && TryParsingOptions()) - { + if (currentTarget != CPT_ExtraParameter && TryParsingOptions()) { DeclareError(CET_NoRequiredParamForOption, targetOption.longName); return false; } } while (TryParsingOptions()); - // First we try `CPT_Remainder` parameter, since it is a special case that - // consumes all further input + // First we try `CPT_Remainder` parameter, since it is a special case that + // consumes all further input if (expectedParameter.type == CPT_Remainder) { return ParseRemainderValue(parsedParameters, expectedParameter); } - // Propagate errors after parsing options + // Propagate errors after parsing options if (nextResult.parsingError != CET_None) { return false; } - // Try parsing one of the variable types + // Try parsing one of the variable types if (expectedParameter.type == CPT_Boolean) { return ParseBooleanValue(parsedParameters, expectedParameter); - } - else if (expectedParameter.type == CPT_Integer) { + } else if (expectedParameter.type == CPT_Integer) { return ParseIntegerValue(parsedParameters, expectedParameter); - } - else if (expectedParameter.type == CPT_Number) { + } else if (expectedParameter.type == CPT_Number) { return ParseNumberValue(parsedParameters, expectedParameter); - } - else if (expectedParameter.type == CPT_Text) { + } else if (expectedParameter.type == CPT_Text) { return ParseTextValue(parsedParameters, expectedParameter); - } - else if (expectedParameter.type == CPT_Remainder) { + } else if (expectedParameter.type == CPT_Remainder) { return ParseRemainderValue(parsedParameters, expectedParameter); - } - else if (expectedParameter.type == CPT_Object) { + } else if (expectedParameter.type == CPT_Object) { return ParseObjectValue(parsedParameters, expectedParameter); - } - else if (expectedParameter.type == CPT_Array) { + } else if (expectedParameter.type == CPT_Array) { return ParseArrayValue(parsedParameters, expectedParameter); - } - else if (expectedParameter.type == CPT_JSON) { + } else if (expectedParameter.type == CPT_JSON) { return ParseJSONValue(parsedParameters, expectedParameter); - } - else if (expectedParameter.type == CPT_Players) { + } else if (expectedParameter.type == CPT_Players) { return ParsePlayersValue(parsedParameters, expectedParameter); } return false; } -// Assumes `commandParser` and `parsedParameters` are not `none`. -// Parses a single boolean value into given `parsedParameters` -// hash table. +// Assumes `commandParser` and `parsedParameters` are not `none`. +// +// Parses a single boolean value into given `parsedParameters` hash table. private final function bool ParseBooleanValue( - HashTable parsedParameters, - Command.Parameter expectedParameter) -{ - local int i; - local bool isValidBooleanLiteral; - local bool booleanValue; - local MutableText parsedLiteral; + HashTable parsedParameters, + Command.Parameter expectedParameter +) { + local int i; + local bool isValidBooleanLiteral; + local bool booleanValue; + local MutableText parsedLiteral; commandParser.Skip().MUntil(parsedLiteral,, true); - if (!commandParser.Ok()) - { + if (!commandParser.Ok()) { _.memory.Free(parsedLiteral); return false; } - // Try to match parsed literal to any recognizable boolean literals - for (i = 0; i < booleanTrueEquivalents.length; i += 1) - { - if (parsedLiteral.CompareToString( booleanTrueEquivalents[i], - SCASE_INSENSITIVE)) - { - isValidBooleanLiteral = true; - booleanValue = true; + // Try to match parsed literal to any recognizable boolean literals + for (i = 0; i < booleanTrueEquivalents.length; i += 1) { + if (parsedLiteral.CompareToString(booleanTrueEquivalents[i], SCASE_INSENSITIVE)) { + isValidBooleanLiteral = true; + booleanValue = true; break; } } - for (i = 0; i < booleanFalseEquivalents.length; i += 1) - { - if (isValidBooleanLiteral) break; - if (parsedLiteral.CompareToString( booleanFalseEquivalents[i], - SCASE_INSENSITIVE)) - { - isValidBooleanLiteral = true; - booleanValue = false; + for (i = 0; i < booleanFalseEquivalents.length; i += 1) { + if (isValidBooleanLiteral) { + break; + } + if (parsedLiteral.CompareToString(booleanFalseEquivalents[i], SCASE_INSENSITIVE)) { + isValidBooleanLiteral = true; + booleanValue = false; } } parsedLiteral.FreeSelf(); if (!isValidBooleanLiteral) { return false; } - RecordParameter(parsedParameters, expectedParameter, - _.box.bool(booleanValue)); + RecordParameter(parsedParameters, expectedParameter, _.box.bool(booleanValue)); return true; } -// Assumes `commandParser` and `parsedParameters` are not `none`. -// Parses a single integer value into given `parsedParameters` -// hash table. +// Assumes `commandParser` and `parsedParameters` are not `none`. +// Parses a single integer value into given `parsedParameters` hash table. private final function bool ParseIntegerValue( - HashTable parsedParameters, - Command.Parameter expectedParameter) -{ + HashTable parsedParameters, + Command.Parameter expectedParameter +) { local int integerValue; commandParser.Skip().MInteger(integerValue); if (!commandParser.Ok()) { return false; } - RecordParameter(parsedParameters, expectedParameter, - _.box.int(integerValue)); + RecordParameter(parsedParameters, expectedParameter, _.box.int(integerValue)); return true; } -// Assumes `commandParser` and `parsedParameters` are not `none`. -// Parses a single number (float) value into given `parsedParameters` -// hash table. +// Assumes `commandParser` and `parsedParameters` are not `none`. +// Parses a single number (float) value into given `parsedParameters` hash table. private final function bool ParseNumberValue( - HashTable parsedParameters, - Command.Parameter expectedParameter) -{ + HashTable parsedParameters, + Command.Parameter expectedParameter +) { local float numberValue; commandParser.Skip().MNumber(numberValue); if (!commandParser.Ok()) { return false; } - RecordParameter(parsedParameters, expectedParameter, - _.box.float(numberValue)); + RecordParameter(parsedParameters, expectedParameter, _.box.float(numberValue)); return true; } -// Assumes `commandParser` and `parsedParameters` are not `none`. -// Parses a single `Text` value into given `parsedParameters` -// hash table. +// Assumes `commandParser` and `parsedParameters` are not `none`. +// Parses a single `Text` value into given `parsedParameters` +// hash table. private final function bool ParseTextValue( HashTable parsedParameters, - Command.Parameter expectedParameter) -{ + Command.Parameter expectedParameter +) { local bool failedParsing; local MutableText textValue; local Parser.ParserState initialState; local HashTable resolvedPair; - // (needs some work for reading formatting `string`s from `Text` objects) + // (needs some work for reading formatting `string`s from `Text` objects) initialState = commandParser.Skip().GetCurrentState(); - // Try manually parsing as a string literal first, since then we will - // allow empty `textValue` as a result + // Try manually parsing as a string literal first, since then we will + // allow empty `textValue` as a result commandParser.MStringLiteral(textValue); failedParsing = !commandParser.Ok(); - // Otherwise - empty values are not allowed - if (failedParsing) - { + // Otherwise - empty values are not allowed + if (failedParsing) { _.memory.Free(textValue); commandParser.RestoreState(initialState).MString(textValue); failedParsing = (!commandParser.Ok() || textValue.IsEmpty()); } - if (failedParsing) - { + if (failedParsing) { _.memory.Free(textValue); commandParser.Fail(); return false; @@ -671,17 +612,13 @@ private final function HashTable AutoResolveAlias(MutableText textValue, Text al } if (aliasSourceName.Compare(P("weapon"))) { resolvedValue = _.alias.ResolveWeapon(textValue, true); - } - else if (aliasSourceName.Compare(P("color"))) { + } else if (aliasSourceName.Compare(P("color"))) { resolvedValue = _.alias.ResolveColor(textValue, true); - } - else if (aliasSourceName.Compare(P("feature"))) { + } else if (aliasSourceName.Compare(P("feature"))) { resolvedValue = _.alias.ResolveFeature(textValue, true); - } - else if (aliasSourceName.Compare(P("entity"))) { + } else if (aliasSourceName.Compare(P("entity"))) { resolvedValue = _.alias.ResolveEntity(textValue, true); - } - else { + } else { resolvedValue = _.alias.ResolveCustom(aliasSourceName, textValue, true); } result.SetItem(P("value"), resolvedValue); @@ -689,13 +626,14 @@ private final function HashTable AutoResolveAlias(MutableText textValue, Text al return result; } -// Assumes `commandParser` and `parsedParameters` are not `none`. -// Parses a single `Text` value into given `parsedParameters` -// hash table, consuming all remaining contents. +// Assumes `commandParser` and `parsedParameters` are not `none`. +// +// Parses a single `Text` value into given `parsedParameters` hash table, consuming all remaining +// contents. private final function bool ParseRemainderValue( - HashTable parsedParameters, - Command.Parameter expectedParameter) -{ + HashTable parsedParameters, + Command.Parameter expectedParameter +) { local MutableText value; commandParser.Skip().MUntil(value); @@ -706,13 +644,13 @@ private final function bool ParseRemainderValue( return true; } -// Assumes `commandParser` and `parsedParameters` are not `none`. -// Parses a single JSON object into given `parsedParameters` -// hash table. +// Assumes `commandParser` and `parsedParameters` are not `none`. +// +// Parses a single JSON object into given `parsedParameters` hash table. private final function bool ParseObjectValue( HashTable parsedParameters, - Command.Parameter expectedParameter) -{ + Command.Parameter expectedParameter +) { local HashTable objectValue; objectValue = _.json.ParseHashTableWith(commandParser); @@ -723,13 +661,12 @@ private final function bool ParseObjectValue( return true; } -// Assumes `commandParser` and `parsedParameters` are not `none`. -// Parses a single JSON array into given `parsedParameters` -// hash table. +// Assumes `commandParser` and `parsedParameters` are not `none`. +// Parses a single JSON array into given `parsedParameters` hash table. private final function bool ParseArrayValue( HashTable parsedParameters, - Command.Parameter expectedParameter) -{ + Command.Parameter expectedParameter +) { local ArrayList arrayValue; arrayValue = _.json.ParseArrayListWith(commandParser); @@ -740,13 +677,13 @@ private final function bool ParseArrayValue( return true; } -// Assumes `commandParser` and `parsedParameters` are not `none`. -// Parses a single JSON value into given `parsedParameters` -// hash table. +// Assumes `commandParser` and `parsedParameters` are not `none`. +// Parses a single JSON value into given `parsedParameters` +// hash table. private final function bool ParseJSONValue( HashTable parsedParameters, - Command.Parameter expectedParameter) -{ + Command.Parameter expectedParameter +) { local AcediaObject jsonValue; jsonValue = _.json.ParseWith(commandParser); @@ -757,9 +694,8 @@ private final function bool ParseJSONValue( return true; } -// Assumes `commandParser` and `parsedParameters` are not `none`. -// Parses a single JSON value into given `parsedParameters` -// hash table. +// Assumes `commandParser` and `parsedParameters` are not `none`. +// Parses a single JSON value into given `parsedParameters` hash table. private final function bool ParsePlayersValue( HashTable parsedParameters, Command.Parameter expectedParameter) @@ -770,8 +706,7 @@ private final function bool ParsePlayersValue( currentPlayersParser.ParseWith(commandParser); if (commandParser.Ok()) { targetPlayers = currentPlayersParser.GetPlayers(); - } - else { + } else { return false; } resultPlayerList = _.collections.NewArrayList(targetPlayers); @@ -779,28 +714,29 @@ private final function bool ParsePlayersValue( RecordParameter(parsedParameters, expectedParameter, resultPlayerList); return true; } -// Assumes `parsedParameters` is not `none`. -// Records `value` for a given `parameter` into a given `parametersArray`. -// If parameter is not a list type - simply records `value` as value under -// `parameter.variableName` key. -// If parameter is a list type - pushed value at the end of an array, -// recorded at `parameter.variableName` key (creating it if missing). -// All recorded values are managed by `parametersArray`. + +// Assumes `parsedParameters` is not `none`. +// +// Records `value` for a given `parameter` into a given `parametersArray`. +// If parameter is not a list type - simply records `value` as value under +// `parameter.variableName` key. +// If parameter is a list type - pushed value at the end of an array, recorded at +// `parameter.variableName` key (creating it if missing). +// +// All recorded values are managed by `parametersArray`. private final function RecordParameter( - HashTable parametersArray, - Command.Parameter parameter, - /* take */ AcediaObject value) -{ + HashTable parametersArray, + Command.Parameter parameter, + /*take*/ AcediaObject value +) { local ArrayList parameterVariable; - if (!parameter.allowsList) - { + if (!parameter.allowsList) { parametersArray.SetItem(parameter.variableName, value); _.memory.Free(value); return; } - parameterVariable = - ArrayList(parametersArray.GetItem(parameter.variableName)); + parameterVariable = ArrayList(parametersArray.GetItem(parameter.variableName)); if (parameterVariable == none) { parameterVariable = _.collections.EmptyArrayList(); } @@ -810,42 +746,45 @@ private final function RecordParameter( _.memory.Free(parameterVariable); } -// Assumes `commandParser` is not `none`. -// Tries to parse an option declaration (along with all of it's parameters) -// with `commandParser`. -// Returns `true` on success and `false` otherwise. -// In case of failure to detect option declaration also reverts state of -// `commandParser` to that before `TryParsingOptions()` call. -// However, if option declaration was present, but invalid (or had -// invalid parameters) parser will be left in a failed state. -private final function bool TryParsingOptions() -{ +// Assumes `commandParser` is not `none`. +// +// Tries to parse an option declaration (along with all of it's parameters) with `commandParser`. +// +// Returns `true` on success and `false` otherwise. +// +// In case of failure to detect option declaration also reverts state of `commandParser` to that +// before `TryParsingOptions()` call. +// However, if option declaration was present, but invalid (or had invalid parameters) parser +// will be left in a failed state. +private final function bool TryParsingOptions() { local int temporaryInt; - if (!commandParser.Ok()) return false; - + if (!commandParser.Ok()) { + return false; + } confirmedState = commandParser.GetCurrentState(); - // Long options + // Long options commandParser.Skip().Match(P("--")); if (commandParser.Ok()) { return ParseLongOption(); } - // Filter out negative numbers that start similarly to short options: - // -3, -5.7, -.9 - commandParser.RestoreState(confirmedState) - .Skip().Match(P("-")).MUnsignedInteger(temporaryInt, 10, 1); - if (commandParser.Ok()) - { + // Filter out negative numbers that start similarly to short options: + // -3, -5.7, -.9 + commandParser + .RestoreState(confirmedState) + .Skip() + .Match(P("-")) + .MUnsignedInteger(temporaryInt, 10, 1); + if (commandParser.Ok()) { commandParser.RestoreState(confirmedState); return false; } commandParser.RestoreState(confirmedState).Skip().Match(P("-.")); - if (commandParser.Ok()) - { + if (commandParser.Ok()) { commandParser.RestoreState(confirmedState); return false; } - // Short options + // Short options commandParser.RestoreState(confirmedState).Skip().Match(P("-")); if (commandParser.Ok()) { return ParseShortOption(); @@ -854,36 +793,33 @@ private final function bool TryParsingOptions() return false; } -// Assumes `commandParser` is not `none`. -// Tries to parse a long option name along with all of it's -// possible parameters with `commandParser`. -// Returns `true` on success and `false` otherwise. At the point this -// method is called, option declaration is already assumed to be detected -// and any failure implies parsing error (ending in failed `Command.CallData`). -private final function bool ParseLongOption() -{ - local int i, optionIndex; - local MutableText optionName; +// Assumes `commandParser` is not `none`. +// +// Tries to parse a long option name along with all of it's possible parameters with +// `commandParser`. +// +// Returns `true` on success and `false` otherwise. At the point this method is called, option +// declaration is already assumed to be detected and any failure implies parsing error +// (ending in failed `Command.CallData`). +private final function bool ParseLongOption() { + local int i, optionIndex; + local MutableText optionName; commandParser.MUntil(optionName,, true); if (!commandParser.Ok()) { return false; } - while (optionIndex < availableOptions.length) - { + while (optionIndex < availableOptions.length) { if (optionName.Compare(availableOptions[optionIndex].longName)) break; optionIndex += 1; } - if (optionIndex >= availableOptions.length) - { + if (optionIndex >= availableOptions.length) { DeclareError(CET_UnknownOption, optionName); optionName.FreeSelf(); return false; } - for (i = 0; i < usedOptions.length; i += 1) - { - if (optionName.Compare(usedOptions[i].longName)) - { + for (i = 0; i < usedOptions.length; i += 1) { + if (optionName.Compare(usedOptions[i].longName)) { DeclareError(CET_RepeatedOption, optionName); optionName.FreeSelf(); return false; @@ -894,12 +830,14 @@ private final function bool ParseLongOption() return ParseOptionParameters(availableOptions[optionIndex]); } -// Assumes `commandParser` and `nextResult` are not `none`. -// Tries to parse a short option name along with all of it's -// possible parameters with `commandParser`. -// Returns `true` on success and `false` otherwise. At the point this -// method is called, option declaration is already assumed to be detected -// and any failure implies parsing error (ending in failed `Command.CallData`). +// Assumes `commandParser` and `nextResult` are not `none`. +// +// Tries to parse a short option name along with all of it's possible parameters with +// `commandParser`. +// +// Returns `true` on success and `false` otherwise. At the point this +// method is called, option declaration is already assumed to be detected +// and any failure implies parsing error (ending in failed `Command.CallData`). private final function bool ParseShortOption() { local int i; @@ -907,69 +845,65 @@ private final function bool ParseShortOption() local MutableText optionsList; commandParser.MUntil(optionsList,, true); - if (!commandParser.Ok()) - { + if (!commandParser.Ok()) { optionsList.FreeSelf(); return false; } - for (i = 0; i < optionsList.GetLength(); i += 1) - { + for (i = 0; i < optionsList.GetLength(); i += 1) { if (nextResult.parsingError != CET_None) break; pickedOptionWithParameters = - AddOptionByCharacter( optionsList.GetCharacter(i), optionsList, - pickedOptionWithParameters) + AddOptionByCharacter( + optionsList.GetCharacter(i), + optionsList, + pickedOptionWithParameters) || pickedOptionWithParameters; } optionsList.FreeSelf(); return (nextResult.parsingError == CET_None); } -// Assumes `commandParser` and `nextResult` are not `none`. -// Auxiliary method that adds option by it's short version's character -// `optionCharacter`. -// It also accepts `optionSourceList` that describes short option -// expression (e.g. "-rtV") from which it originated for error reporting and -// `forbidOptionWithParameters` that, when set to `true`, forces this method to -// cause the `CET_MultipleOptionsWithParams` error if -// new option has non-empty parameters. -// Method returns `true` if added option had non-empty parameters and -// `false` otherwise. -// Any parsing failure inside this method always causes -// `nextError.DeclareError()` call, so you can use `nextResult.IsSuccessful()` -// to check if method has failed. +// Assumes `commandParser` and `nextResult` are not `none`. +// +// Auxiliary method that adds option by it's short version's character `optionCharacter`. +// +// It also accepts `optionSourceList` that describes short option expression (e.g. "-rtV") from +// which it originated for error reporting and `forbidOptionWithParameters` that, when set to +// `true`, forces this method to cause the `CET_MultipleOptionsWithParams` error if new option has +// non-empty parameters. +// +// Method returns `true` if added option had non-empty parameters and `false` otherwise. +// +// Any parsing failure inside this method always causes `nextError.DeclareError()` call, so you +// can use `nextResult.IsSuccessful()` to check if method has failed. private final function bool AddOptionByCharacter( - BaseText.Character optionCharacter, - BaseText optionSourceList, - bool forbidOptionWithParameters) -{ + BaseText.Character optionCharacter, + BaseText optionSourceList, + bool forbidOptionWithParameters +) { local int i; local bool optionHasParameters; - // Prevent same option appearing twice - for (i = 0; i < usedOptions.length; i += 1) - { - if (_.text.AreEqual(optionCharacter, usedOptions[i].shortName)) - { + // Prevent same option appearing twice + for (i = 0; i < usedOptions.length; i += 1) { + if (_.text.AreEqual(optionCharacter, usedOptions[i].shortName)) { DeclareError(CET_RepeatedOption, usedOptions[i].longName); return false; } } - // If it's a new option - look it up in all available options - for (i = 0; i < availableOptions.length; i += 1) - { + // If it's a new option - look it up in all available options + for (i = 0; i < availableOptions.length; i += 1) { if (!_.text.AreEqual(optionCharacter, availableOptions[i].shortName)) { continue; } usedOptions[usedOptions.length] = availableOptions[i]; optionHasParameters = (availableOptions[i].required.length > 0 || availableOptions[i].optional.length > 0); - // Enforce `forbidOptionWithParameters` flag restriction - if (optionHasParameters && forbidOptionWithParameters) - { + // Enforce `forbidOptionWithParameters` flag restriction + if (optionHasParameters && forbidOptionWithParameters) { DeclareError(CET_MultipleOptionsWithParams, optionSourceList); return optionHasParameters; } - // Parse parameters (even if they are empty) and bail + // Parse parameters (even if they are empty) and bail commandParser.Skip(); ParseOptionParameters(availableOptions[i]); break; @@ -980,35 +914,30 @@ private final function bool AddOptionByCharacter( return optionHasParameters; } -// Auxiliary method for parsing option's parameters (including empty ones). -// Automatically fills `nextResult` with parsed parameters -// (or `none` if option has no parameters). -// Assumes `commandParser` and `nextResult` are not `none`. -private final function bool ParseOptionParameters(Command.Option pickedOption) -{ +// Auxiliary method for parsing option's parameters (including empty ones). +// Automatically fills `nextResult` with parsed parameters (or `none` if option has no parameters). +// Assumes `commandParser` and `nextResult` are not `none`. +private final function bool ParseOptionParameters(Command.Option pickedOption) { local HashTable optionParameters; - // If we are already parsing other option's parameters and did not finish - // parsing all required ones - we cannot start another option - if (currentTargetIsOption && currentTarget != CPT_ExtraParameter) - { + // If we are already parsing other option's parameters and did not finish + // parsing all required ones - we cannot start another option + if (currentTargetIsOption && currentTarget != CPT_ExtraParameter) { DeclareError(CET_NoRequiredParamForOption, targetOption.longName); return false; } - if (pickedOption.required.length == 0 && pickedOption.optional.length == 0) - { + if (pickedOption.required.length == 0 && pickedOption.optional.length == 0) { nextResult.options.SetItem(pickedOption.longName, none); return true; } - currentTargetIsOption = true; - targetOption = pickedOption; - optionParameters = ParseParameterArrays( pickedOption.required, - pickedOption.optional); + currentTargetIsOption = true; + targetOption = pickedOption; + optionParameters = ParseParameterArrays( + pickedOption.required, + pickedOption.optional); currentTargetIsOption = false; - if (commandParser.Ok()) - { - nextResult.options - .SetItem(pickedOption.longName, optionParameters); + if (commandParser.Ok()) { + nextResult.options.SetItem(pickedOption.longName, optionParameters); _.memory.Free(optionParameters); return true; } @@ -1016,8 +945,7 @@ private final function bool ParseOptionParameters(Command.Option pickedOption) return false; } -defaultproperties -{ +defaultproperties { booleanTrueEquivalents(0) = "true" booleanTrueEquivalents(1) = "enable" booleanTrueEquivalents(2) = "on" From 677dd84e90eb51398b0c7d85ec9748193525e74d Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 13 Mar 2023 20:30:01 +0700 Subject: [PATCH 09/17] Fix style for remaining `CommandAPI`-related classes --- sources/BaseAPI/API/Commands/Command.uc | 766 ++++++++---------- sources/BaseAPI/API/Commands/PlayersParser.uc | 444 +++++----- 2 files changed, 513 insertions(+), 697 deletions(-) diff --git a/sources/BaseAPI/API/Commands/Command.uc b/sources/BaseAPI/API/Commands/Command.uc index d3f67d9..0c60987 100644 --- a/sources/BaseAPI/API/Commands/Command.uc +++ b/sources/BaseAPI/API/Commands/Command.uc @@ -1,12 +1,8 @@ /** - * 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 overload `Executed()` / `ExecutedFor()` - * to perform required actions when command is executed by a player. - * `Executed()` is called first, whenever command is executed and - * `ExecuteFor()` is called only for targeted commands, once for each - * targeted player. - * Copyright 2021-2022 Anton Tarasenko + * Author: dkanus + * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore + * License: GPL + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -26,277 +22,234 @@ class Command extends AcediaObject dependson(BaseText); -/** - * # `Command` - * - * Command class provides an automated way to add a command to a server through - * AcediaCore's features. It takes care of: - * - * 1. Verifying that player has passed correct (expected parameters); - * 2. Parsing these parameters into usable values (both standard, built-in - * types like `bool`, `int`, `float`, etc. and more advanced types such - * as players lists and JSON values); - * 3. Allowing you to easily specify a set of players you are targeting by - * supporting several ways to refer to them, such as *by name*, *by id* - * and *by selector* (@ and @self refer to caller player, @all refers - * to all players). - * 4. It can be registered inside AcediaCore's commands feature and be - * automatically called through the unified system that supports *chat* - * and *mutate* inputs (as well as allowing you to hook in any other - * input source); - * 5. Will also automatically provide a help page through built-in "help" - * command; - * 6. Subcommand support - when one command can have several distinct - * functions, depending on how its called (e.g. "inventory add" vs - * "inventory remove"). These subcommands have a special treatment in - * help pages, which makes them more preferable, compared to simply - * matching first `Text` argument; - * 7. Add support for "options" - additional flags that can modify commands - * behavior and behave like usual command options "--force"/"-f". - * Their short versions can even be combined: - * "give@ $ebr --ammo --force" can be rewritten as "give@ $ebr -af". - * And they can have their own parameters: "give@all --list sharp". - * - * ## Usage - * - * To create a custom command you need to simply: - * - * 1. Create a custom command class derived from `Command`; - * 2. Define `BuildData()` function and use given `CommandDataBuilder` to - * fill-in data about what parameters your command takes. You can also - * add optional descriptions that would appear in your command's - * help page. - * 3. Overload `Executed()` or `ExecutedFor()` (or both) method and add - * whatever logic you want to execute once your command was called. - * All parameters and options will be listed inside passed `CallData` - * parameter. These methods will only be called if all necessary - * parameters were correctly specified. - * - * ## Implementation - * - * The idea of `Command`'s implementation is simple: command is basically - * the `Command.Data` struct that is filled via `CommandDataBuilder`. - * Whenever command is called it uses `CommandParser` to parse user's input - * based on its `Command.Data` and either report error (in case of failure) or - * pass make `Executed()`/`ExecutedFor()` calls (in case of success). - * When command is called is decided by `Commands_Feature` that tracks - * possible user inputs (and provides `HandleInput()`/`HandleInputWith()` - * methods for adding custom command inputs). That feature basically parses - * first part of the command: its name (not the subcommand's names) and target - * players (using `PlayersParser`, but only if command is targeted). - * - * Majority of the command-related code either serves to build - * `Command.Data` or to parse command input by using it (`CommandParser`). - */ - -/** - * Possible errors that can arise when parsing command parameters from - * user input - */ -enum ErrorType -{ - // No error +//! This class is meant to represent a command type. +//! +//! Command class provides an automated way to add a command to a server through +//! AcediaCore's features. It takes care of: +//! +//! 1. Verifying that player has passed correct (expected parameters); +//! 2. Parsing these parameters into usable values (both standard, built-in +//! types like `bool`, `int`, `float`, etc. and more advanced types such +//! as players lists and JSON values); +//! 3. Allowing you to easily specify a set of players you are targeting by +//! supporting several ways to refer to them, such as *by name*, *by id* +//! and *by selector* (@ and @self refer to caller player, @all refers +//! to all players). +//! 4. It can be registered inside AcediaCore's commands feature and be +//! automatically called through the unified system that supports *chat* +//! and *mutate* inputs (as well as allowing you to hook in any other +//! input source); +//! 5. Will also automatically provide a help page through built-in "help" +//! command; +//! 6. Subcommand support - when one command can have several distinct +//! functions, depending on how its called (e.g. "inventory add" vs +//! "inventory remove"). These subcommands have a special treatment in +//! help pages, which makes them more preferable, compared to simply +//! matching first `Text` argument; +//! 7. Add support for "options" - additional flags that can modify commands +//! behavior and behave like usual command options "--force"/"-f". +//! Their short versions can even be combined: +//! "give@ $ebr --ammo --force" can be rewritten as "give@ $ebr -af". +//! And they can have their own parameters: "give@all --list sharp". +//! +//! # Implementation +//! +//! The idea of `Command`'s implementation is simple: command is basically the `Command.Data` struct +//! that is filled via `CommandDataBuilder`. +//! Whenever command is called it uses `CommandParser` to parse user's input based on its +//! `Command.Data` and either report error (in case of failure) or pass make +//! `Executed()`/`ExecutedFor()` calls (in case of success). +//! +//! When command is called is decided by `Commands_Feature` that tracks possible user inputs +//! (and provides `HandleInput()`/`HandleInputWith()` methods for adding custom command inputs). +//! That feature basically parses first part of the command: its name (not the subcommand's names) +//! and target players (using `PlayersParser`, but only if command is targeted). +//! +//! Majority of the command-related code either serves to build `Command.Data` or to parse command +//! input by using it (`CommandParser`). + +/// Possible errors that can arise when parsing command parameters from user input +enum ErrorType { + // No error CET_None, - // Bad parser was provided to parse user input - // (this should not be possible) + // 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) + // Sub-command name was not specified or was incorrect + // (this should not be possible) CET_NoSubCommands, - // Specified sub-command does not exist - // (only relevant when it is enforced for parser, e.g. by an alias) + // Specified sub-command does not exist + // (only relevant when it is enforced for parser, e.g. by an alias) CET_BadSubCommand, - // Required param for command / option was not specified + // Required param for command / option was not specified CET_NoRequiredParam, CET_NoRequiredParamForOption, - // Unknown option key was specified + // Unknown option key was specified CET_UnknownOption, CET_UnknownShortOption, - // Same option appeared twice in one command call + // 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 + // 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 + // 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) + // (For targeted commands only) + // Targets are specified incorrectly (or none actually specified) CET_IncorrectTargetList, CET_EmptyTargetList }; -/** - * Structure that contains all the information about how `Command` was called. - */ -struct CallData -{ - // Targeted players (if applicable) - var public array targetPlayers; - // Specified sub-command and parameters/options - var public Text subCommandName; - // Provided parameters and specified options - var public HashTable parameters; - var public HashTable options; - // Errors that occurred during command call processing are described by - // error type and optional error textual name of the object - // (parameter, option, etc.) that caused it. - var public ErrorType parsingError; - var public Text errorCause; +/// Structure that contains all the information about how `Command` was called. +struct CallData { + // Targeted players (if applicable) + var public array targetPlayers; + // Specified sub-command and parameters/options + var public Text subCommandName; + // Provided parameters and specified options + var public HashTable parameters; + var public HashTable options; + // Errors that occurred during command call processing are described by + // error type and optional error textual name of the object + // (parameter, option, etc.) that caused it. + var public ErrorType parsingError; + var public Text errorCause; }; -/** - * Possible types of parameters. - */ -enum ParameterType -{ - // Parses into `BoolBox` +/// Possible types of parameters. +enum ParameterType { + // Parses into `BoolBox` CPT_Boolean, - // Parses into `IntBox` + // Parses into `IntBox` CPT_Integer, - // Parses into `FloatBox` + // Parses into `FloatBox` CPT_Number, - // Parses into `Text` + // Parses into `Text` CPT_Text, - // Special parameter that consumes the rest of the input into `Text` + // Special parameter that consumes the rest of the input into `Text` CPT_Remainder, - // Parses into `HashTable` + // Parses into `HashTable` CPT_Object, - // Parses into `ArrayList` + // Parses into `ArrayList` CPT_Array, - // Parses into any JSON value + // Parses into any JSON value CPT_JSON, - // Parses into an array of specified players + // Parses into an array of specified players CPT_Players }; -/** - * 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 -{ +/// 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 +// 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; - // `CPT_Text` can be attempted to be auto-resolved as an alias from - /// some source during parsing. For command to attempt that, this field must - // be not-`none` and contain the name of the alias source (either "weapon", - // "color", "feature", "entity" or some kind of custom alias source name). - // Only relevant when given value is prefixed with "$" character. - var Text aliasSourceName; + // `CPT_Text` can be attempted to be auto-resolved as an alias from some source during parsing. + // For command to attempt that, this field must be not-`none` and contain the name of + // the alias source (either "weapon", "color", "feature", "entity" or some kind of custom alias + // source name). + // + // Only relevant when given value is prefixed with "$" character. + var Text aliasSourceName; }; -// Defines a sub-command of a this command (specified as -// " "). -// 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` +// Defines a sub-command of a this command (specified as " "). +// +// 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 required; - var array optional; + var array required; + var array optional; }; -// Defines command's option (options are specified by "--long" or "-l"). -// Options are independent from sub-commands. -struct Option -{ - var BaseText.Character shortName; - var Text longName; - var Text description; - // Option can also have their own parameters - var array required; - var array optional; +// Defines command's option (options are specified by "--long" or "-l"). +// Options are independent from sub-commands. +struct Option { + var BaseText.Character shortName; + var Text longName; + var Text description; + // Option can also have their own parameters + var array required; + var array optional; }; -// Structure that defines what sub-commands and options command has -// (and what parameters they take) -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; +// Structure that defines what sub-commands and options command has +// (and what parameters they take) +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; var protected array subCommands; - var protected array