939 lines
35 KiB
Ucode
939 lines
35 KiB
Ucode
/**
|
|
* Author: dkanus
|
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
|
* License: GPL
|
|
* Copyright 2021-2023 Anton Tarasenko
|
|
*------------------------------------------------------------------------------
|
|
* This file is part of Acedia.
|
|
*
|
|
* Acedia is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Acedia is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
class CommandDataBuilder extends AcediaObject
|
|
dependson(Command);
|
|
|
|
//! 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<Command.SubCommand> subcommands;
|
|
var private array<Command.Option> 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<byte> subcommandsIsOptional;
|
|
var private array<byte> 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<Command.Parameter> 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
|
|
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;
|
|
}
|
|
|
|
/// 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).
|
|
shortNameAsCharacter = GetValidShortName(longName, shortName);
|
|
if ( !_.text.IsValidCharacter(shortNameAsCharacter)
|
|
|| VerifyNoOptionNamingConflict(longName, shortNameAsCharacter)) {
|
|
// ^ `GetValidShortName()` and `VerifyNoOptionNamingConflict()`
|
|
// are responsible for logging warnings/errors
|
|
return;
|
|
}
|
|
SelectOption(longName);
|
|
// Set short name for new options
|
|
optionIndex = FindOptionIndex(longName);
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// 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) {
|
|
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 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.
|
|
/// Group name is case-insensitive.
|
|
public final function Group(BaseText newName) {
|
|
if (newName != none && newName == commandGroup) {
|
|
return;
|
|
}
|
|
_.memory.Free(commandGroup);
|
|
if (newName != none) {
|
|
commandGroup = newName.Copy();
|
|
} else {
|
|
commandGroup = none;
|
|
}
|
|
}
|
|
|
|
/// Sets new summary of `Command.Data` under construction.
|
|
///
|
|
/// Summary gives a short description of the command on the whole that will
|
|
/// be displayed when "help" command is listing available command
|
|
public final function Summary(BaseText newSummary) {
|
|
if (newSummary != none && newSummary == commandSummary) {
|
|
return;
|
|
}
|
|
_.memory.Free(commandSummary);
|
|
if (newSummary != none) {
|
|
commandSummary = newSummary.Copy();
|
|
} else {
|
|
commandSummary = none;
|
|
}
|
|
}
|
|
|
|
/// Makes caller builder to mark `Command.Data` under construction to require
|
|
/// a player target.
|
|
public final function RequireTarget() {
|
|
requiresTarget = true;
|
|
}
|
|
|
|
|
|
/// 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;
|
|
}
|
|
// Record all required parameters first, otherwise there would be no way
|
|
// to distinguish between them and optional parameters
|
|
RecordSelection();
|
|
selectionIsOptional = true;
|
|
selectedParameterArray.length = 0;
|
|
}
|
|
|
|
/// 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;
|
|
|
|
// TODO: is this copying needed?
|
|
RecordSelection();
|
|
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) {
|
|
selectedParameterArray[selectedParameterArray.length] = newParameter;
|
|
}
|
|
|
|
// 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
|
|
) {
|
|
local Command.Parameter newParameter;
|
|
|
|
newParameter.displayName = displayName.Copy();
|
|
newParameter.type = parameterType;
|
|
newParameter.allowsList = isListParameter;
|
|
if (variableName != none) {
|
|
newParameter.variableName = variableName.Copy();
|
|
} else {
|
|
newParameter.variableName = displayName.Copy();
|
|
}
|
|
return newParameter;
|
|
}
|
|
|
|
/// 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
|
|
) {
|
|
local Command.Parameter newParam;
|
|
|
|
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
|
|
) {
|
|
local Command.Parameter newParam;
|
|
|
|
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.
|
|
/// 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,
|
|
optional BaseText aliasSourceName
|
|
) {
|
|
local Command.Parameter newParameterValue;
|
|
|
|
if (name == none) {
|
|
return;
|
|
}
|
|
newParameterValue = NewParameter(name, CPT_Text, false, variableName);
|
|
if (aliasSourceName != none) {
|
|
newParameterValue.aliasSourceName = aliasSourceName.Copy();
|
|
}
|
|
PushParameter(newParameterValue);
|
|
}
|
|
|
|
/// 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.
|
|
/// 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,
|
|
optional BaseText aliasSourceName
|
|
) {
|
|
local Command.Parameter newParameterValue;
|
|
|
|
if (name == none) {
|
|
return;
|
|
}
|
|
newParameterValue = NewParameter(name, CPT_Text, true, variableName);
|
|
if (aliasSourceName != none) {
|
|
newParameterValue.aliasSourceName = aliasSourceName.Copy();
|
|
}
|
|
PushParameter(newParameterValue);
|
|
}
|
|
|
|
/// 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`.
|
|
///
|
|
/// 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 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`.
|
|
///
|
|
/// 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 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 parameter that defines a set of players (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 ParamPlayers(BaseText name, optional BaseText variableName) {
|
|
if (name != none) {
|
|
PushParameter(NewParameter(name, CPT_PLAYERS, false, variableName));
|
|
}
|
|
}
|
|
|
|
/// Adds new parameter that defines a list of sets of players (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 ParamPlayersList(BaseText name,optional BaseText variableName) {
|
|
if (name != none) {
|
|
PushParameter(NewParameter(name, CPT_PLAYERS, true, variableName));
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
if (name.Compare(subcommands[i].name)) {
|
|
return i;
|
|
}
|
|
}
|
|
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) {
|
|
local int i;
|
|
|
|
if (longName == none) {
|
|
return -1;
|
|
}
|
|
for (i = 0; i < options.length; i += 1) {
|
|
if (longName.Compare(options[i].longName)) {
|
|
return i;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
// 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)) {
|
|
return;
|
|
}
|
|
RecordSelection();
|
|
optionIndex = FindOptionIndex(longName);
|
|
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;
|
|
if (selectionIsOptional) {
|
|
selectedParameterArray = options[optionIndex].optional;
|
|
} else {
|
|
selectedParameterArray = options[optionIndex].required;
|
|
}
|
|
}
|
|
|
|
// Saves currently selected data into prepared data.
|
|
private final function RecordSelection() {
|
|
if (selectedItemName == none) {
|
|
return;
|
|
}
|
|
if (selectedItemIsOption) {
|
|
RecordSelectedOption();
|
|
} 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;
|
|
}
|
|
selectedSubCommandIndex = FindSubCommandIndex(selectedItemName);
|
|
if (selectedSubCommandIndex < 0) {
|
|
selectedSubCommandIndex = subcommands.length;
|
|
subcommands[selectedSubCommandIndex] = newSubcommand;
|
|
}
|
|
subcommands[selectedSubCommandIndex].name = selectedItemName;
|
|
subcommands[selectedSubCommandIndex].description = selectedDescription;
|
|
if (selectionIsOptional) {
|
|
subcommands[selectedSubCommandIndex].optional = selectedParameterArray;
|
|
subcommandsIsOptional[selectedSubCommandIndex] = 1;
|
|
} 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;
|
|
}
|
|
selectedOptionIndex = FindOptionIndex(selectedItemName);
|
|
if (selectedOptionIndex < 0) {
|
|
selectedOptionIndex = options.length;
|
|
options[selectedOptionIndex] = newOption;
|
|
}
|
|
options[selectedOptionIndex].longName = selectedItemName;
|
|
options[selectedOptionIndex].description = selectedDescription;
|
|
if (selectionIsOptional) {
|
|
options[selectedOptionIndex].optional = selectedParameterArray;
|
|
optionsIsOptional[selectedOptionIndex] = 1;
|
|
} else {
|
|
options[selectedOptionIndex].required = selectedParameterArray;
|
|
optionsIsOptional[selectedOptionIndex] = 0;
|
|
}
|
|
}
|
|
|
|
// Validates names (printing errors in case of failure) for the option.
|
|
// Long name must be at least 2 characters long.
|
|
// Short name must be either:
|
|
// 1. exactly one character long;
|
|
// 2. `none`, which leads to deriving `shortName` from `longName`
|
|
// as a first character.
|
|
// Anything else will result in logging a failure and rejection of
|
|
// the option altogether.
|
|
// Returns `none` if validation failed and chosen short name otherwise
|
|
// (if `shortName` was used for it - it's value will be copied).
|
|
private final function BaseText.Character GetValidShortName(
|
|
BaseText longName,
|
|
BaseText shortName
|
|
) {
|
|
// Validate `longName`
|
|
if (longName == none) {
|
|
return _.text.GetInvalidCharacter();
|
|
}
|
|
if (longName.GetLength() < 2) {
|
|
_.logger.Auto(errLongNameTooShort).ArgClass(class).Arg(longName.Copy());
|
|
return _.text.GetInvalidCharacter();
|
|
}
|
|
// Validate `shortName`,
|
|
// deriving if from `longName` if necessary & possible
|
|
if (shortName == none) {
|
|
return longName.GetCharacter(0);
|
|
}
|
|
if (shortName.IsEmpty() || shortName.GetLength() > 1) {
|
|
_.logger.Auto(errShortNameTooLong).ArgClass(class).Arg(longName.Copy());
|
|
return _.text.GetInvalidCharacter();
|
|
}
|
|
return shortName.GetCharacter(0);
|
|
}
|
|
|
|
// Checks that if any option record has a long/short name from a given pair of
|
|
// names (`longName`, `shortName`), then it also has another one.
|
|
//
|
|
// 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
|
|
) {
|
|
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) {
|
|
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;
|
|
}
|
|
if (!sameLongNames && sameShortNames) {
|
|
_.logger.Auto(warnSameLongName).ArgClass(class).Arg(_.text.FromCharacter(shortName));
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
defaultproperties
|
|
{
|
|
errLongNameTooShort = (l=LOG_Error,m="Command `%1` is trying to register an option with a name that is way too short (<2 characters). Option will be discarded: %2")
|
|
errShortNameTooLong = (l=LOG_Error,m="Command `%1` is trying to register an option with a short name that doesn't consist of just one character. Option will be discarded: %2")
|
|
warnSameLongName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same long name \"%2\", but different short names. This should not happen, do not expect correct behavior.")
|
|
warnSameShortName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same short name \"%2\", but different long names. This should not have happened, do not expect correct behavior.")
|
|
} |