diff --git a/config/AcediaAliases_Colors.ini b/config/AcediaAliases_Colors.ini index 913130d..ed48d09 100644 --- a/config/AcediaAliases_Colors.ini +++ b/config/AcediaAliases_Colors.ini @@ -8,7 +8,7 @@ record=(alias="TextSubHeader",value="rgb(147,112,219)") record=(alias="TextPositive",value="rgb(60,220,20)") record=(alias="TextNeutral",value="rgb(255,255,0)") record=(alias="TextNegative",value="rgb(220,20,60)") -record=(alias="TextSubtle",value="rgb(128,128,128)") +record=(alias="TextSubtle",value="rgb(211,211,211)") record=(alias="TextEmphasis",value="rgb(0,128,255)") record=(alias="TextOk",value="rgb(0,255,0)") record=(alias="TextWarning",value="rgb(255,128,0)") diff --git a/config/AcediaAliases_Commands.ini b/config/AcediaAliases_Commands.ini index e69de29..012eb51 100644 --- a/config/AcediaAliases_Commands.ini +++ b/config/AcediaAliases_Commands.ini @@ -0,0 +1,8 @@ +; This config file allows you to configure command aliases. +; Remember that aliases are case-insensitive. +[AcediaCore.CommandAliasSource] +record=(alias="yes",value="vote yes") +record=(alias="no",value="vote no") + +[help CommandAliases] +Alias="hlp" \ No newline at end of file diff --git a/config/AcediaSystem.ini b/config/AcediaSystem.ini index 5bfd2d6..a42cf4e 100644 --- a/config/AcediaSystem.ini +++ b/config/AcediaSystem.ini @@ -1,5 +1,8 @@ ; Every single option in this config should be considered [ADVANCED]. ; DO NOT CHANGE THEM unless you are sure you know what you're doing. +[AcediaCore.AcediaEnvironment] +debugMode=false + [AcediaCore.SideEffects] ; Acedia requires adding its own `GameRules` to listen to many different ; game events. @@ -150,7 +153,7 @@ maximumNotifyTime=20 TextDefault=(R=255,G=255,B=255,A=255) TextHeader=(R=128,G=0,B=128,A=255) TextSubHeader=(R=147,G=112,B=219,A=255) -TextSubtle=(R=128,G=128,B=128,A=255) +TextSubtle=(R=211,G=211,B=211,A=255) TextEmphasis=(R=0,G=128,B=255,A=255) TextPositive=(R=0,G=128,B=0,A=255) TextNeutral=(R=255,G=255,B=0,A=255) diff --git a/config/AcediaVoting.ini b/config/AcediaVoting.ini new file mode 100644 index 0000000..60731ba --- /dev/null +++ b/config/AcediaVoting.ini @@ -0,0 +1,22 @@ +[default VotingSettings] +;= Determines the duration of the voting period, specified in seconds. +votingTime=30 +;= Determines whether spectators are allowed to vote. +allowSpectatorVoting=false +;= Specifies which group(s) of players are allowed to see who makes what vote. +allowedToSeeVotesGroup="admin" +allowedToSeeVotesGroup="moderator" +;= Specifies which group(s) of players are allowed to vote. +allowedToVoteGroup="everybody" + +[moderator VotingSettings] +votingTime=30 +allowSpectatorVoting=true +allowedToSeeVotesGroup="admin" +allowedToVoteGroup="moderator" +allowedToVoteGroup="admin" + +[admin VotingSettings] +votingTime=30 +allowSpectatorVoting=true +allowedToVoteGroup="admin" \ No newline at end of file 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/BaseAPI/API/Commands/BuiltInCommands/ACommandFakers.uc b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandFakers.uc new file mode 100644 index 0000000..9e146bd --- /dev/null +++ b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandFakers.uc @@ -0,0 +1,166 @@ +/** + * 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 ACommandFakers extends Command + dependsOn(VotingModel); + +var private array fakers; + +protected function BuildData(CommandDataBuilder builder) { + builder.Name(P("fakers")); + builder.Group(P("debug")); + builder.Summary(P("Adds fake voters for testing \"vote\" command.")); + builder.Describe(P("Displays current fake voters.")); + + builder.SubCommand(P("amount")); + builder.Describe(P("Specify amount of faker that are allowed to vote.")); + builder.ParamInteger(P("fakers_amount")); + + builder.SubCommand(P("vote")); + builder.Describe(P("Make a vote as a faker.")); + builder.ParamInteger(P("faker_number")); + builder.ParamBoolean(P("vote_for")); +} + +protected function Executed(CallData arguments, EPlayer instigator) { + if (arguments.subCommandName.IsEmpty()) { + DisplayCurrentFakers(); + } else if (arguments.subCommandName.Compare(P("amount"), SCASE_INSENSITIVE)) { + ChangeAmount(arguments.parameters.GetInt(P("fakers_amount"))); + } else if (arguments.subCommandName.Compare(P("vote"), SCASE_INSENSITIVE)) { + CastVote( + arguments.parameters.GetInt(P("faker_number")), + arguments.parameters.GetBool(P("vote_for"))); + } +} + +public final function UpdateFakersForVoting() { + local Voting currentVoting; + + currentVoting = GetCurrentVoting(); + if (currentVoting != none) { + currentVoting.SetDebugVoters(fakers); + } + _.memory.Free(currentVoting); +} + +private final function CastVote(int fakerID, bool voteFor) { + local Voting currentVoting; + + if (fakerID < 0 || fakerID >= fakers.length) { + callerConsole + .UseColor(_.color.TextFailure) + .WriteLine(P("Faker number is out of bounds.")); + return; + } + currentVoting = GetCurrentVoting(); + if (currentVoting == none) { + callerConsole + .UseColor(_.color.TextFailure) + .WriteLine(P("There is no voting active right now.")); + return; + } + currentVoting.CastVoteByID(fakers[fakerID], voteFor); + _.memory.Free(currentVoting); +} + +private final function ChangeAmount(int newAmount) { + local int i; + local Text nextIDName; + local UserID nextID; + + if (newAmount < 0) { + callerConsole + .UseColor(_.color.TextFailure) + .WriteLine(P("Cannot specify negative amount.")); + } + if (newAmount == fakers.length) { + callerConsole + .UseColor(_.color.TextNeutral) + .WriteLine(P("Specified same amount of fakers.")); + } else if (newAmount > fakers.length) { + for (i = fakers.length; i < newAmount; i += 1) { + nextIDName = _.text.FromString("DEBUG:FAKER:" $ i); + nextID = UserID(__().memory.Allocate(class'UserID')); + nextID.Initialize(nextIDName); + _.memory.Free(nextIDName); + fakers[fakers.length] = nextID; + } + } else { + for (i = fakers.length - 1; i >= newAmount; i -= 1) { + _.memory.Free(fakers[i]); + } + fakers.length = newAmount; + } + UpdateFakersForVoting(); +} + +private function Voting GetCurrentVoting() { + local Commands_Feature feature; + local Voting result; + + feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); + if (feature != none) { + result = feature.GetCurrentVoting(); + feature.FreeSelf(); + } + return result; +} + +private function DisplayCurrentFakers() { + local int i; + local VotingModel.PlayerVoteStatus nextVoteStatus; + local MutableText nextNumber; + local Voting currentVoting; + + if (fakers.length <= 0) { + callerConsole.WriteLine(P("No fakers!")); + return; + } + currentVoting = GetCurrentVoting(); + for (i = 0; i < fakers.length; i += 1) { + nextNumber = _.text.FromIntM(i); + callerConsole + .Write(P("Faker #")) + .Write(nextNumber) + .Write(P(": ")); + if (currentVoting != none) { + nextVoteStatus = currentVoting.GetVote(fakers[i]); + } + switch (nextVoteStatus) { + case PVS_NoVote: + callerConsole.WriteLine(P("no vote")); + break; + case PVS_VoteFor: + callerConsole.UseColorOnce(_.color.TextPositive).WriteLine(P("vote for")); + break; + case PVS_VoteAgainst: + callerConsole.UseColorOnce(_.color.TextNegative).WriteLine(P("vote against")); + break; + default: + callerConsole.UseColorOnce(_.color.TextFailure).WriteLine(P("vote !ERROR!")); + } + _.memory.Free(nextNumber); + } +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/LevelAPI/Features/Commands/BuiltInCommands/ACommandHelp.uc b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandHelp.uc similarity index 96% rename from sources/LevelAPI/Features/Commands/BuiltInCommands/ACommandHelp.uc rename to sources/BaseAPI/API/Commands/BuiltInCommands/ACommandHelp.uc index a4ec0f9..0540cc7 100644 --- a/sources/LevelAPI/Features/Commands/BuiltInCommands/ACommandHelp.uc +++ b/sources/BaseAPI/API/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/BuiltInCommands/ACommandNotify.uc b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc similarity index 100% rename from sources/LevelAPI/Features/Commands/BuiltInCommands/ACommandNotify.uc rename to sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc diff --git a/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandVote.uc b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandVote.uc new file mode 100644 index 0000000..d11f7e5 --- /dev/null +++ b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandVote.uc @@ -0,0 +1,159 @@ +/** + * 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 ACommandVote extends Command; + +var private CommandDataBuilder dataBuilder; + +protected function Constructor() { + ResetVotingInfo(); +} + +protected function Finalizer() { + super.Finalizer(); + _.memory.Free(dataBuilder); + dataBuilder = none; +} + +protected function BuildData(CommandDataBuilder builder) { + builder.Name(P("vote")); + builder.Group(P("core")); + builder.Summary(P("Allows players to initiate any available voting." + @ "Votings themselves are added as sub-commands.")); + builder.Describe(P("Default command simply displaces information about current vote.")); + + dataBuilder.SubCommand(P("yes")); + builder.Describe(P("Vote `yes` on the current vote.")); + dataBuilder.SubCommand(P("no")); + builder.Describe(P("Vote `no` on the current vote.")); +} + +protected function Executed(CallData arguments, EPlayer instigator) { + local Voting currentVoting; + local Commands_Feature feature; + + feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); + if (feature == none) { + callerConsole + .UseColor(_.color.TextFailure) + .WriteLine(P("Feature responsible for commands and voting isn't enabled." + @ "This is unexpected, something broke terribly.")); + return; + } else { + currentVoting = feature.GetCurrentVoting(); + } + if (arguments.subCommandName.IsEmpty()) { + DisplayInfoAboutVoting(instigator, currentVoting); + } else if (arguments.subCommandName.Compare(P("yes"), SCASE_INSENSITIVE)) { + CastVote(currentVoting, instigator, true); + } else if (arguments.subCommandName.Compare(P("no"), SCASE_INSENSITIVE)) { + CastVote(currentVoting, instigator, false); + } else { + StartVoting(arguments.subCommandName, feature, currentVoting, instigator); + } + _.memory.Free(feature); + _.memory.Free(currentVoting); +} + +/// Adds sub-command information about given voting with a given name. +public final function AddVotingInfo(BaseText processName, class processClass) { + if (processName == none) return; + if (processClass == none) return; + if (dataBuilder == none) return; + + dataBuilder.SubCommand(processName); + processClass.static.AddInfo(dataBuilder); + commandData = dataBuilder.BorrowData(); +} + +/// Clears all sub-command information added from [`Voting`]s. +public final function ResetVotingInfo() { + _.memory.Free(dataBuilder); + dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder')); + BuildData(dataBuilder); + commandData = dataBuilder.BorrowData(); +} + +private final function DisplayInfoAboutVoting(EPlayer instigator, Voting currentVoting) { + if (currentVoting == none) { + callerConsole.WriteLine(P("No voting is active right now.")); + } else { + currentVoting.PrintVotingInfoFor(instigator); + } +} + +private final function CastVote(Voting currentVoting, EPlayer voter, bool voteForSuccess) { + if (currentVoting != none) { + currentVoting.CastVote(voter, voteForSuccess); + } else { + callerConsole.UseColor(_.color.TextWarning).WriteLine(P("No voting is active right now.")); + } +} + +// Assumes all arguments aren't `none`. +private final function StartVoting( + BaseText votingName, + Commands_Feature feature, + Voting currentVoting, + EPlayer instigator +) { + local Command fakersCommand; + local Voting newVoting; + local Commands_Feature.StartVotingResult result; + + result = feature.StartVoting(votingName); + // Handle errors + if (result == SVR_UnknownVoting) { + callerConsole + .UseColor(_.color.TextFailure) + .Write(P("Unknown voting option \"")) + .Write(votingName) + .WriteLine(P("\"")); + return; + } else if (result == SVR_AlreadyInProgress) { + callerConsole + .UseColor(_.color.TextWarning) + .WriteLine(P("Another voting is already in progress!")); + return; + } + // Inform new voting about fake voters, in case we're debugging + if (currentVoting == none && _.environment.IsDebugging()) { + fakersCommand = feature.GetCommand(P("fakers")); + if (fakersCommand != none && fakersCommand.class == class'ACommandFakers') { + ACommandFakers(fakersCommand).UpdateFakersForVoting(); + } + _.memory.Free(fakersCommand); + } + // Cast a vote from instigator + newVoting = feature.GetCurrentVoting(); + if (newVoting != none) { + newVoting.CastVote(instigator, true); + } else { + callerConsole + .UseColor(_.color.TextFailure) + .WriteLine(P("Voting should be available, but it isn't." + @ "This is unexpected, something broke terribly.")); + } + _.memory.Free(newVoting); +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Commands/Command.uc b/sources/BaseAPI/API/Commands/Command.uc new file mode 100644 index 0000000..c46c831 --- /dev/null +++ b/sources/BaseAPI/API/Commands/Command.uc @@ -0,0 +1,585 @@ +/** + * 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 . + */ +class Command extends AcediaObject + dependson(BaseText); + +//! 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) + CET_BadParser, + // 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) + CET_BadSubCommand, + // Required param for command / option was not specified + CET_NoRequiredParam, + CET_NoRequiredParamForOption, + // Unknown option key was specified + CET_UnknownOption, + CET_UnknownShortOption, + // 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 + CET_UnusedCommandParameters, + // 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) + 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; +}; + +/// Possible types of parameters. +enum ParameterType { + // Parses into `BoolBox` + CPT_Boolean, + // Parses into `IntBox` + CPT_Integer, + // Parses into `FloatBox` + CPT_Number, + // Parses into `Text` + CPT_Text, + // Special parameter that consumes the rest of the input into `Text` + CPT_Remainder, + // Parses into `HashTable` + CPT_Object, + // Parses into `ArrayList` + CPT_Array, + // Parses into any JSON value + CPT_JSON, + // 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 { + 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 + 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; +}; + +// 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; +}; + +// 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; + var protected array subCommands; + var protected array